Controller WebUI Endpoints Development Guide
This document provides a comprehensive guide for developing controller WebUI endpoints in Rspamd, covering both the existing C-based endpoints and how to create new Lua-based endpoints.
Overview
The Rspamd controller is an HTTP daemon that provides a REST API interface for managing and monitoring Rspamd. It serves both built-in C endpoints and dynamically loaded Lua endpoints for extended functionality.
Architecture
The controller system consists of:
- C core: Built-in endpoints implemented in
src/controller.c
- Lua plugin system: Dynamic endpoints loaded from
rules/controller/
directory - HTTP router: Routes requests to appropriate handlers
- Authentication system: Password-based security with different privilege levels
URL Structure
- Built-in C endpoints:
/{endpoint}
(e.g.,/stat
,/scan
) - Lua plugin endpoints:
/plugins/{plugin}/{path}
(e.g.,/plugins/neural/learn
) - Static files: Served from configured static directory
Built-in C Endpoints
The following endpoints are implemented in C and provide core functionality:
Authentication & Status
POST /auth
- Authenticate and get system statusGET /ping
- Simple health checkGET /healthy
- Comprehensive health check with worker statusGET /ready
- Readiness check for scanner workers
Statistics & Monitoring
GET /stat
- Get detailed statisticsGET /statreset
- Reset statistics (privileged)GET /metrics
- Prometheus-format metricsGET /counters
- Symbol counters from cacheGET /errors
- Recent error logs (privileged)
Configuration & Management
GET /symbols
- List all symbols with weights and descriptionsGET /actions
- List actions with thresholdsPOST /saveactions
- Save action thresholds (privileged)POST /savesymbols
- Save symbol weights (privileged)
Maps Management
GET /maps
- List all mapsGET /getmap
- Get map content by IDPOST /savemap
- Save map content (privileged)
Message Processing
POST /scan
- Scan message and return resultsPOST /check
- Alias for/scan
POST /checkv2
- Extended scan endpointPOST /learnspam
- Learn message as spam (privileged)POST /learnham
- Learn message as ham (privileged)
Data Visualization
GET /graph
- Time-series graph data for WebUIGET /pie
- Pie chart data for action statistics
History & Analysis
GET /history
- Message processing historyPOST /historyreset
- Clear history (privileged)
System Information
GET /neighbours
- Configured neighbour nodesGET /plugins
- List available Lua pluginsGET /bayes/classifiers
- List Bayes classifiers
Creating Lua Endpoints
Lua endpoints provide a flexible way to extend controller functionality. They are automatically registered through the plugin system.
Basic Structure
Create a Lua file in rules/controller/
directory:
-- rules/controller/example.lua
local function handle_hello(task, conn, req_params)
conn:send_ucl({
success = true,
message = "Hello from Lua endpoint!",
params = req_params
})
end
return {
hello = {
handler = handle_hello,
enable = false, -- Normal password sufficient for read-only operation
need_task = false,
}
}
This creates an endpoint at /plugins/example/hello
.
Endpoint Configuration
Each endpoint is defined with the following properties:
endpoint_name = {
handler = function, -- Required: Handler function
enable = boolean, -- Required: Authentication level (false=normal, true=privileged)
need_task = boolean, -- Optional: Whether to create a task object
version = number, -- Optional: API version
}
Configuration Options
handler
: Function that processes the requestenable
: Authentication level -true
requires privileged (enable) password,false
allows normal passwordneed_task
: Set totrue
if the handler needs access to message contentversion
: API version number (optional)
Choosing Authentication Level
Use enable = false
for:
- Read-only operations (list data, query information)
- Public API endpoints that don't modify system state
- Endpoints that should be accessible with basic credentials
Use enable = true
for:
- Operations that modify configuration
- System administration functions
- Sensitive data access
- Operations that could affect system security
return {
-- Public read-only endpoint
status = {
handler = handle_status,
enable = false, -- Normal password OK
},
-- Administrative endpoint
reload_config = {
handler = handle_reload_config,
enable = true, -- Requires privileged password
}
}
Handler Function Signatures
Simple Handler (need_task = false)
local function simple_handler(task, conn, req_params)
-- task: Always present but may be minimal
-- conn: Connection object for sending responses
-- req_params: Query parameters from URL/POST data
end
Task Handler (need_task = true)
local function task_handler(task, conn, req_params)
-- task: Full task object with message processing capabilities
-- conn: Connection object for sending responses
-- req_params: Query parameters from URL/POST data
-- Process the message
task:process_message()
-- Access message properties
local from = task:get_from()
local subject = task:get_header('Subject')
end
Connection Object Methods
The conn
object provides methods for sending responses:
Send UCL/JSON Response
conn:send_ucl({
success = true,
data = { key = "value" },
count = 42
})
Send Plain Text
conn:send_string("Plain text response")
Send Error Response
conn:send_error(404, "Resource not found")
-- or
conn:send_error(500, "Internal server error: " .. error_msg)
Request Parameters
Parameters come from:
- URL query string:
?param1=value1¶m2=value2
- POST form data:
application/x-www-form-urlencoded
- JSON POST body: Parse manually with UCL
Accessing Query Parameters
local function handle_query(task, conn, req_params)
local search_term = req_params.q or ""
local limit = tonumber(req_params.limit) or 10
-- Process query...
conn:send_ucl({
query = search_term,
limit = limit,
results = {}
})
end
Processing JSON POST Data
local ucl = require "ucl"
local function handle_json_post(task, conn, req_params)
local parser = ucl.parser()
local ok, err = parser:parse_text(task:get_rawbody())
if not ok then
conn:send_error(400, "Invalid JSON: " .. err)
return
end
local data = parser:get_object()
-- Process data...
conn:send_ucl({ success = true, received = data })
end
Real-World Examples
Example 1: Selectors Plugin
The selectors plugin demonstrates various endpoint types:
-- rules/controller/selectors.lua
local lua_selectors = require "lua_selectors"
local function handle_list_transforms(_, conn)
conn:send_ucl(lua_selectors.list_transforms())
end
local function handle_check_message(task, conn, req_params)
if req_params.selector and req_params.selector ~= '' then
local selector = lua_selectors.create_selector_closure(
rspamd_config, req_params.selector, '', true)
if not selector then
conn:send_error(500, 'invalid selector')
else
task:process_message()
local elts = selector(task)
conn:send_ucl({ success = true, data = elts })
end
else
conn:send_error(404, 'missing selector')
end
end
return {
list_transforms = {
handler = handle_list_transforms,
enable = true, -- System information access
},
check_message = {
handler = handle_check_message,
enable = true, -- System information access
need_task = true,
}
}
Example 2: Maps Query Plugin
The maps plugin shows complex parameter handling:
-- rules/controller/maps.lua
local function handle_query_map(_, conn, req_params)
local keys_to_check = {}
if req_params.value and req_params.value ~= '' then
keys_to_check[1] = req_params.value
elseif req_params.values then
keys_to_check = lua_util.str_split(req_params.values, ',')
end
local results = {}
for _, key in ipairs(keys_to_check) do
for uri, m in pairs(maps_cache) do
local value = m:get_key(key)
if value then
table.insert(results, {
map = uri,
key = key,
value = value,
hit = true
})
end
end
end
conn:send_ucl({
success = (#results > 0),
results = results
})
end
return {
query = {
handler = handle_query_map,
enable = false, -- Normal password sufficient (read-only operation)
}
}
Example 3: Neural Network Training
The neural plugin demonstrates JSON schema validation:
-- rules/controller/neural.lua
local ts = require("tableshape").types
local ucl = require "ucl"
local learn_request_schema = ts.shape {
ham_vec = ts.array_of(ts.array_of(ts.number)),
spam_vec = ts.array_of(ts.array_of(ts.number)),
rule = ts.string:is_optional(),
}
local function handle_learn(task, conn)
local parser = ucl.parser()
local ok, err = parser:parse_text(task:get_rawbody())
if not ok then
conn:send_error(400, err)
return
end
local req_params = parser:get_object()
ok, err = learn_request_schema:transform(req_params)
if not ok then
conn:send_error(400, err)
return
end
-- Process training data...
neural_common.spawn_train {
ev_base = task:get_ev_base(),
ham_vec = req_params.ham_vec,
spam_vec = req_params.spam_vec,
-- ... other parameters
}
conn:send_string('{"success": true}')
end
return {
learn = {
handler = handle_learn,
enable = true, -- Requires privileged password (modifies neural networks)
need_task = true,
}
}
Authentication and Security
Password Levels
The controller supports two password levels:
- Normal password: Read-only access to most endpoints
- Enable password: Full access including privileged operations
Authentication Levels
The enable
flag in endpoint configuration controls authentication requirements:
enable = false
: Accepts normal password (read-only access)enable = true
: Requires privileged (enable) password (full access)
return {
list_data = {
handler = handle_list_data,
enable = false, -- Normal password sufficient
},
modify_settings = {
handler = handle_modify_settings,
enable = true, -- Requires privileged password
}
}
IP-based Access
Configure secure_ip
in worker configuration for password-less access:
worker {
type = controller
secure_ip = ["127.0.0.1", "::1", "192.168.1.0/24"]
}
Session Read-Only Mode
The controller automatically sets session permissions:
- Normal password:
session.is_read_only = true
(unless no enable password configured) - Enable password:
session.is_read_only = false
- Trusted IP:
session.is_read_only = false
Access Control in Handlers
Lua endpoints can check session permissions if needed:
local function sensitive_handler(task, conn, req_params)
-- Authentication already handled by controller
-- Additional checks can be implemented if needed
local session = -- session access not directly exposed to Lua
-- Use enable=true in config for privileged operations
-- Proceed with operation...
end
Advanced Features
Schema Validation
Use tableshape for robust input validation:
local ts = require("tableshape").types
local request_schema = ts.shape {
name = ts.string,
count = ts.number:is_optional(),
tags = ts.array_of(ts.string):is_optional(),
}
local function validated_handler(task, conn, req_params)
local ok, err = request_schema:transform(req_params)
if not ok then
conn:send_error(400, "Validation error: " .. err)
return
end
-- Process validated input...
end
Asynchronous Operations
For operations requiring external HTTP requests, use the async HTTP API:
local rspamd_http = require "rspamd_http"
local function async_handler(task, conn, req_params)
local function http_callback(err, response)
if err then
conn:send_error(500, "External request failed: " .. err)
return
end
-- Process response and send result
local data = response.content
conn:send_ucl({
success = true,
external_data = data,
status_code = response.code
})
end
-- Make async HTTP request
rspamd_http.request({
url = "https://api.example.com/data",
method = "POST",
headers = {
["Content-Type"] = "application/json",
["Authorization"] = "Bearer " .. req_params.token
},
body = require("ucl").to_format({
query = req_params.query
}, "json"),
callback = http_callback,
task = task, -- Only task needed, not both task and ev_base
timeout = 10.0,
})
end
Redis Operations
For Redis operations, use the modern lua_redis module:
local lua_redis = require "lua_redis"
-- Module-level Redis configuration (usually done at module init)
local redis_params = nil
local function init_redis_config()
local opts = rspamd_config:get_all_opt('mymodule')
if opts and opts.redis then
redis_params = lua_redis.parse_redis_server('mymodule')
end
end
local function redis_handler(task, conn, req_params)
if not redis_params then
conn:send_error(500, "Redis not configured")
return
end
local function redis_callback(err, data)
if err then
conn:send_error(500, "Redis error: " .. err)
return
end
conn:send_ucl({
success = true,
redis_data = data
})
end
-- Modern Redis API
local attrs = {
task = task,
callback = redis_callback,
is_write = false, -- false for read operations
key = req_params.key or "default_key"
}
lua_redis.request(redis_params, attrs, {
'HGET',
req_params.key or "mykey",
req_params.field or "myfield"
})
end
-- Alternative: Using Redis with coroutines (no callback)
local function redis_sync_handler(task, conn, req_params)
if not redis_params then
conn:send_error(500, "Redis not configured")
return
end
local attrs = {
task = task,
is_write = false,
key = req_params.key or "default_key"
}
-- This will work with coroutines
local ok, data = lua_redis.request(redis_params, attrs, {
'HGET',
req_params.key or "mykey",
req_params.field or "myfield"
})
if not ok then
conn:send_error(500, "Redis request failed")
return
end
conn:send_ucl({
success = true,
redis_data = data
})
end
AWS S3 Integration Example
Based on the AWS S3 plugin, here's how to integrate with external services:
local rspamd_http = require "rspamd_http"
local lua_aws = require "lua_aws"
local function s3_upload_handler(task, conn, req_params)
if not req_params.bucket or not req_params.content then
conn:send_error(400, "bucket and content parameters required")
return
end
local function s3_callback(http_err, code, body, headers)
if http_err then
conn:send_error(500, "S3 upload failed: " .. http_err)
return
end
if code == 200 then
conn:send_ucl({
success = true,
s3_key = req_params.key,
status_code = code
})
else
conn:send_error(code, "S3 error: " .. (body or "unknown"))
end
end
local s3_key = string.format("/%s/%s", req_params.path or "uploads",
req_params.filename or "data.txt")
local aws_host = string.format('%s.s3.amazonaws.com', req_params.bucket)
local headers = lua_aws.aws_request_enrich({
region = req_params.region or "us-east-1",
headers = {
['Content-Type'] = req_params.content_type or "text/plain",
['Host'] = aws_host
},
uri = s3_key,
key_id = req_params.aws_key_id,
secret_key = req_params.aws_secret_key,
method = 'PUT',
}, req_params.content)
rspamd_http.request({
url = string.format("https://%s%s", aws_host, s3_key),
method = 'PUT',
body = req_params.content,
headers = headers,
callback = s3_callback,
task = task,
timeout = 30.0,
})
end
Error Handling
Implement comprehensive error handling:
local function robust_handler(task, conn, req_params)
local ok, result = pcall(function()
-- Potentially failing operation
return process_complex_request(req_params)
end)
if not ok then
rspamd_logger.errx(task, "Handler error: %s", result)
conn:send_error(500, "Internal server error")
return
end
conn:send_ucl({ success = true, data = result })
end
Plugin Registration System
Automatic Registration
Plugins are automatically loaded from:
rules/controller/*.lua
- Default pluginslocal.d/controller.lua
- Local overrides
Registration Process
The controller scans for plugins during startup:
-- In rules/controller/init.lua
local controller_plugin_paths = {
maps = dofile(local_rules .. "/controller/maps.lua"),
neural = dofile(local_rules .. "/controller/neural.lua"),
selectors = dofile(local_rules .. "/controller/selectors.lua"),
fuzzy = dofile(local_rules .. "/controller/fuzzy.lua"),
}
-- Local overrides
if rspamd_util.file_exists(local_conf .. '/controller.lua') then
local overrides = dofile(local_conf .. '/controller.lua')
controller_plugin_paths = lua_util.override_defaults(
controller_plugin_paths, overrides)
end
Custom Plugin Registration
User-Defined Controller Configuration
Users can create local.d/controller.lua
to add custom endpoints or override existing ones. This file is automatically loaded by the controller initialization system.
File Location: local.d/controller.lua
(in Rspamd configuration directory)
Format: The file should return a table mapping plugin names to their endpoint definitions:
-- local.d/controller.lua
-- Define custom endpoint handlers
local function handle_custom_status(task, conn, req_params)
local status = {
server_time = os.time(),
custom_metric = get_custom_metric(),
environment = req_params.env or "production"
}
conn:send_ucl({ success = true, status = status })
end
local function handle_custom_reload(task, conn, req_params)
if not req_params.component then
conn:send_error(400, "component parameter required")
return
end
-- Perform custom reload logic
local result = reload_custom_component(req_params.component)
conn:send_ucl({ success = result, component = req_params.component })
end
-- Return plugin definitions
return {
-- Override existing plugin with custom implementation
maps = dofile("/usr/local/etc/rspamd/custom/enhanced_maps.lua"),
-- Add completely new plugin
custom_admin = {
status = {
handler = handle_custom_status,
enable = false, -- Normal password sufficient
need_task = false,
},
reload = {
handler = handle_custom_reload,
enable = true, -- Requires privileged password
need_task = false,
},
},
-- Load plugin from external file
monitoring = dofile("/opt/company/rspamd/monitoring_endpoints.lua"),
}
Redis Configuration in Controllers
When using Redis in controller endpoints, initialize the configuration at module level:
-- local.d/controller.lua
local lua_redis = require "lua_redis"
-- Redis configuration initialization
local redis_params = nil
local function init_redis()
local opts = rspamd_config:get_all_opt('controller_redis')
if opts then
redis_params = lua_redis.parse_redis_server('controller_redis')
end
end
-- Initialize Redis when module loads
init_redis()
local function handle_redis_data(task, conn, req_params)
if not redis_params then
conn:send_error(503, "Redis not available")
return
end
local attrs = {
task = task,
callback = function(err, data)
if err then
conn:send_error(500, "Redis error: " .. err)
else
conn:send_ucl({ success = true, data = data })
end
end,
is_write = false,
key = req_params.cache_key
}
lua_redis.request(redis_params, attrs, {'GET', req_params.cache_key})
end
return {
cache = {
get = {
handler = handle_redis_data,
enable = false,
need_task = false,
}
}
}
Plugin Override Behavior
The system uses lua_util.override_defaults()
to merge configurations:
- Existing plugins: Can be completely replaced by providing a new definition
- New plugins: Added alongside default plugins
- Individual endpoints: Cannot be selectively overridden - entire plugin must be replaced
Practical Examples
Example 1: Simple Custom Endpoints
-- local.d/controller.lua
local rspamd_logger = require "rspamd_logger"
local rspamd_util = require "rspamd_util"
local function handle_server_info(task, conn, req_params)
local info = {
hostname = rspamd_util.get_hostname(),
version = rspamd_version,
uptime = rspamd_util.get_uptime(), -- System uptime in seconds
worker_pid = rspamd_util.get_pid(), -- Current process PID
memory_usage = rspamd_util.get_memory_usage(), -- {rss: bytes, vsize: bytes}
}
rspamd_logger.infox(rspamd_config, "Server info requested from %s",
conn:get_peer_addr())
conn:send_ucl({ success = true, server_info = info })
end
local function handle_custom_metrics(task, conn, req_params)
-- Collect custom application metrics
local metrics = collect_application_metrics()
conn:send_ucl({
success = true,
timestamp = os.time(),
metrics = metrics
})
end
return {
system_info = {
info = {
handler = handle_server_info,
enable = false, -- Read-only information
need_task = false,
},
metrics = {
handler = handle_custom_metrics,
enable = true, -- May contain sensitive data
need_task = false,
},
}
}
This creates endpoints:
GET /plugins/system_info/info
- Server information (normal password)GET /plugins/system_info/metrics
- Custom metrics (privileged password)
Example 2: External Configuration Management
-- local.d/controller.lua
local config_manager = dofile("/etc/rspamd/custom/config_manager.lua")
return {
-- Replace default maps with enhanced version
maps = dofile("/etc/rspamd/custom/enhanced_maps.lua"),
-- Add configuration management endpoints
config = config_manager.get_endpoints(),
-- Add monitoring endpoints loaded from external system
monitoring = dofile("/opt/monitoring/rspamd_endpoints.lua"),
}
Example 3: Development/Debug Endpoints
-- local.d/controller.lua
-- Only add debug endpoints in development
local environment = os.getenv("RSPAMD_ENV") or "production"
local endpoints = {}
if environment == "development" then
local function handle_debug_symbols(task, conn, req_params)
task:process_message()
local symbols = task:get_symbols()
conn:send_ucl({
success = true,
debug_info = {
symbols = symbols,
meta = task:get_meta(),
headers = task:get_raw_headers(),
}
})
end
endpoints.debug = {
symbols = {
handler = handle_debug_symbols,
enable = true, -- Debug info is sensitive
need_task = true, -- Needs message content
}
}
end
-- Always available admin endpoints
local function handle_cache_clear(task, conn, req_params)
clear_application_cache()
conn:send_ucl({ success = true, message = "Cache cleared" })
end
endpoints.admin = {
clear_cache = {
handler = handle_cache_clear,
enable = true, -- Administrative operation
need_task = false,
}
}
return endpoints
Loading External Files
When loading endpoints from external files, ensure they return the expected format:
-- /opt/company/rspamd/monitoring_endpoints.lua
local rspamd_http = require "rspamd_http"
local function handle_health_check(task, conn, req_params)
-- Async health check of external services
rspamd_http.request({
url = "http://internal-api:8080/health",
method = "GET",
timeout = 5.0,
callback = function(err, response)
if err or response.code ~= 200 then
conn:send_error(503, "External service unavailable")
else
conn:send_ucl({
success = true,
external_status = "healthy",
response_time = response.elapsed
})
end
end,
task = task,
ev_base = task:get_ev_base(),
})
end
-- Return endpoints table
return {
health = {
handler = handle_health_check,
enable = false, -- Health checks are generally public
need_task = false,
}
}
Best Practices for Custom Controllers
- Namespace your plugins to avoid conflicts with future Rspamd updates
- Use descriptive handler names for easier debugging
- Validate input parameters thoroughly
- Log significant actions for audit trails
- Handle errors gracefully with appropriate HTTP status codes
- Use async operations for external dependencies
- Document your custom endpoints for team members
Security Considerations
- Sensitive operations: Always use
enable = true
for administrative functions - Input validation: Validate all parameters to prevent injection attacks
- Audit logging: Log access to sensitive endpoints
- Error messages: Don't expose internal details in error responses
- Rate limiting: Consider implementing rate limiting for resource-intensive endpoints
Best Practices
1. Error Handling
- Always validate input parameters
- Use appropriate HTTP status codes
- Provide meaningful error messages
- Log errors for debugging
2. Response Format
- Use consistent JSON structure
- Include
success
field for status - Provide descriptive field names
- Handle empty results gracefully
3. Performance
- Cache expensive computations
- Use
need_task = false
when possible - Implement pagination for large datasets
- Consider async operations for slow tasks
4. Security
- Validate all inputs
- Sanitize user data
- Use schema validation
- Be cautious with file operations
5. Documentation
- Document endpoint parameters
- Provide usage examples
- Describe return formats
- Note any special requirements
Testing Endpoints
Manual Testing with curl
# Test simple endpoint
curl "http://localhost:11334/plugins/example/hello?param=value"
# Test with authentication
curl -H "Password: your-password" \
"http://localhost:11334/plugins/example/data"
# Test POST with JSON
curl -X POST \
-H "Content-Type: application/json" \
-H "Password: your-password" \
-d '{"key": "value"}' \
"http://localhost:11334/plugins/example/process"
Functional Testing
Create test cases in the functional test suite using async HTTP API:
-- test/functional/lua/controller_test.lua
local rspamd_http = require "rspamd_http"
local function test_custom_endpoint()
local function http_callback(err, response)
if err then
error("HTTP request failed: " .. err)
end
-- Assert expected response
assert(response.code == 200)
assert(response.content:match("success"))
end
rspamd_http.request({
url = "http://127.0.0.1:11334/plugins/example/hello",
method = "GET",
headers = {
["Content-Type"] = "application/json",
["Password"] = "test_password" -- Add authentication if needed
},
callback = http_callback,
task = task, -- Pass task if available
timeout = 5.0,
})
end
-- Example with POST data
local function test_custom_post_endpoint()
local function http_callback(err, response)
if err then
error("HTTP POST failed: " .. err)
end
local ucl = require "ucl"
local parser = ucl.parser()
parser:parse_string(response.content)
local data = parser:get_object()
assert(data.success == true)
assert(data.processed_count > 0)
end
rspamd_http.request({
url = "http://127.0.0.1:11334/plugins/example/process",
method = "POST",
headers = {
["Content-Type"] = "application/json",
["Password"] = "enable_password" -- Privileged endpoint
},
body = require("ucl").to_format({
items = {"item1", "item2", "item3"},
operation = "batch_process"
}, "json"),
callback = http_callback,
task = task,
timeout = 10.0,
})
end
Debugging
Logging in Handlers
local rspamd_logger = require "rspamd_logger"
local function debug_handler(task, conn, req_params)
rspamd_logger.infox(task, "Handler called with params: %s",
req_params)
-- Log error conditions
if not req_params.required_param then
rspamd_logger.warnx(task, "Missing required parameter")
conn:send_error(400, "Missing required parameter")
return
end
-- Debug processing
rspamd_logger.debugx(task, "Processing request for: %s",
req_params.item)
end
Common Issues
- Endpoint not found: Check plugin registration in
init.lua
- Authentication failures:
- Verify password configuration in worker config
- Check if
enable = true
endpoints require privileged password - Ensure correct password headers are sent
- Task errors: Ensure
need_task = true
when accessing message content - JSON parsing errors: Validate input format and encoding
- Permission errors: Check file system permissions for maps/static files
- Enable flag confusion:
enable = false
allows normal password accessenable = true
requires privileged (enable) password- Flag controls authentication level, not endpoint availability
Conclusion
The Rspamd controller provides a powerful framework for extending WebUI functionality through Lua plugins. By following the patterns and best practices outlined in this guide, you can create robust, secure, and efficient endpoints that integrate seamlessly with the existing system.
For more examples, examine the existing plugins in the rules/controller/
directory and refer to the Rspamd Lua API documentation for detailed information about available functions and objects.