Admin Lua Extensions
We support users to write Lua extensions to perform some custom functions, which can be triggered by cron or events.
For example, we can query the database every minute for nodes with high cpu and report them to the user’s own monitoring system via HTTP requests.
Here is an practical example of how to create a Lua extension.
We create a cron extension with the following Lua code:
local sql = [[
select node_id, avg("system_CPU_percent") from monitor where created > now() - INTERVAL '1 hour' group by node_id limit 1
]]
local res = sql_query(sql, 120, 2000, "log_server")
local params = {
body = res
}
res = http_query('POST', "http://receive-metrics.openresty.com", params)
output(res)
Then we can click the execute button to see the results immediately.
The execution results of the extension can be seen in the history page.
Lua extensions can also be triggered by events. When the specified event occurs, it will trigger the extension to run and the event details will be placed in the Lua variable trigger_event
.
Here is an example of printing events.
output(trigger_event)
Click the execute button to see the results immediately. A node offline test event is provided here by default.
Events
We currently support these events:
Nodes Heartbeat Offline
Triggered when the nodes do not send heartbeats to Admin and are set to offline.
{
"type": "nodes_heartbeat_offline",
"from": "log-server",
"message": "Gateway nodes [59] offline",
"level": "ERROR"
}
Nodes Heartbeat Online
Triggered when the nodes send heartbeats to Admin and are set to online.
{
"type": "nodes_heartbeat_online",
"from": "log-server",
"message": "Gateway nodes [59] online",
"level": "WARNING"
}
Nodes Offline
Triggered when the node’s health check fails and the nodes are set to offline.
{
"from": "admin",
"level": "ERROR",
"message": "gateway node [78] is offline since failed to connect to 120.24.93.4:81: connection refused, time: 1634629224;;failed to connect to 120.24.93.4:81: connection refused, time: 1634629254;;failed to connect to 120.24.93.4:81: connection refused, time: 1634629284",
"type": "node_offline"
}
Nodes Online
Triggered when the node’s health check is successful and the nodes are set to online.
{
"from": "admin",
"level": "WARNING",
"message": "gateway node [78] is online since health check success",
"type": "node_online"
}
Release
Triggered when the application is released.
{
"type": "release",
"uid": 2,
"http_app_id": 786
}
WAF Event
Triggered when the request hit WAF rules and the score is greater than the threshold value.
The requests of the same client IP will only trigger a WAF event once in a second.
{
"app_id": "1033",
"type": "waf_event",
"score": "3",
"threshold": "3",
"action": "block",
"matches": [
{
"matches": [
"0",
"/hit"
],
"begin_line": 1,
"version": "d04fb751526fc85a172475a71f19cf53",
"rule_id": "0",
"rule_set_id": 10025,
"msg": "test",
"group": "test",
"end_line": 2
}
],
"header": "User-Agent: curl/7.29.0\r\nHost: waf-filter.com\r\nAccept: */*\r\nProxy-Connection: Keep-Alive\r\n\r\n",
"timestamp": "1634629481",
"request_id": "0000270010243927eb48000d",
"client_country": "",
"client_province": "unknown",
"client_city": "unknown",
"from": "log_server",
"client_isp": "",
"body": "",
"remote_addr": "172.17.0.1",
"host": "waf-filter.com",
"request": "GET HTTP://waf-filter.com/hit HTTP/1.1"
}
Builtin API
Most of the regular Lua code is supported in the extensions, and we also provide a built-in lua api for writing extensions.
output
syntax: output(msg)
The output messages will be displayed in the Lua extension histories.
http_query
syntax: res = http_query(method, url, params, retries)
Send the HTTP request.
method
The HTTP method, likeGET
,POST
,PUT
.url
The HTTP url string.retries
Number of retries after request failed, default retries is 1.
The params
table accepts the following fields:
timeout
Sets the timeout (in millisecond), default value is 300 seconds.headers
A table of request headers.body
The request body as a string, or an iterator function (see get_client_body_reader).ssl_verify
Verify SSL cert matches hostname
When the request is successful, res
will contain the following fields:
status
The status code.headers
A table of headers. Multiple headers with the same field name will be presented as a table of values.body
The response body.
sql_query
Query the admin or log-server database.
syntax: res = sql_query(sql, timeout, limit, destination, retries)
sql
Query SQL, only select statements are supported.timeout
Sets the timeout (in second), default value is 120 seconds.limit
Sets the limit of query result sets, default value is 20000.destination
Query destination:admin
andlog-server
.retries
Number of retries after query failed, default retries is 1.
send_alarm_event
Sending custom alarm events
syntax: res = send_alarm_event(alarm_type, alarm_level, alarm_message)
alarm_type
Customized alarm typesalarm_level
Alarm levels, there are three levels: CRITICAL, ERROR, WARNINGalarm_message
Alarm text content
More Examples
Add IP data blocked by the WAF to the application’s IP list
This extension queries the database for the IP data blocked by the WAF and adds them to the application’s IP list.
Here we need to enable WAF and IP list in the application first.
-- In this example we assume that the application id and IP list id are both 1.
local app_id = "1"
local ip_list_id = "1"
local str_fmt = string.format
local api_put = require "Lua.SchemaDB" .update
-- Get the IP addresses blocked by WAF in the last 24 hours
local sql = [[
SELECT DISTINCT remote_addr as ip
FROM waf_request_tsdb
WHERE action='block'
AND score >= threshold
AND created >= now() - interval '24 hours'
AND app_id='%s'
]]
sql = str_fmt(sql, app_id)
local res = sql_query(sql, 120, 2000, "log_server")
local ip_list = { items = res }
local uri = { "applications", app_id, "ip_list", ip_list_id }
res, err = api_put(uri, ip_list)
if res then
output("updated ip blacklist successfully!")
else
output("failed to update ip blacklist: " .. tostring(err))
end
Verify the HTTPS certificate of the application specified with the APP_ID
-- UPDATE-ME: please specify app_id as you want
local app_id = nil
-- uncomment following lines if you want enable event trigger
-- local app_id = trigger_event.http_app_id
if not app_id then
return output("WARN: app_id is required")
end
local ngx = ngx
local substr = string.sub
local str_fmt = string.format
local re_find = ngx.re.find
local httpc = require "resty.http".new()
local function ssl_handshake(ip, port, domain)
local c, err = httpc:connect(ip, port)
if not c then
return nil, err
end
return httpc:ssl_handshake(nil, domain, true)
end
local function is_wildcard_domain(domain)
if string.sub(domain, 1, 2) == '*.' then
return true
end
return false
end
local app_domains_sql = [[
select applications_domains.domain "domain",
applications_domains.is_wildcard is_wildcard,
https_ports,
offline_enabled
from applications
left join applications_domains on applications.id = applications_domains._applications_id
where applications.id = %d
]]
local cert_domains_sql = [[
select applications_phases_ssl_cert_certs_acme_host.item acme_host
from applications_phases_ssl_cert_certs
join applications_phases_ssl_cert_certs_acme_host on
applications_phases_ssl_cert_certs_acme_host._applications_phases_ssl_cert_certs_id
= applications_phases_ssl_cert_certs.id
where global_cert is null and applications_phases_ssl_cert_certs._applications_id = %d
union
select global_certs.acme_host acme_host
from applications_phases_ssl_cert_certs
join global_certs on applications_phases_ssl_cert_certs.global_cert = global_certs.id
where global_cert is not null and applications_phases_ssl_cert_certs._applications_id = %d
]]
local gateway_nodes_sql = [[
select gateway_nodes.external_ip external_ip,
gateway_nodes.external_ipv6 external_ipv6
from applications
left join applications_partitions on applications.id = applications_partitions._applications_id
left join gateway on applications_partitions.item = gateway.partition
left join gateway_nodes on gateway.id = gateway_nodes._gateway_id
where offline_enabled is not true
and (gateway_nodes.external_ip is not null or gateway_nodes.external_ipv6 is not null)
and applications.id = %d
]]
local ok_tbl = {}
local err_tbl = {}
local check_list = {}
local app_domains_hash = {}
local app_domains, err = sql_query(str_fmt(app_domains_sql, tonumber(app_id)))
if not app_domains then
output(str_fmt("failed to verify TLS certificate for app, app_id: %d"), tostring(app_id))
end
local domain_list = {}
for _, app_domain in ipairs(app_domains) do
domain_list[#domain_list + 1] = app_domain.domain
app_domains_hash[app_domain.domain] = app_domain
end
local domain_str = table.concat(domain_list, ', ')
local cert_domains, err = sql_query(str_fmt(cert_domains_sql, tonumber(app_id), tonumber(app_id)))
if not cert_domains then
output("failed to verify TLS certificate for app, domain: %s, err: no certificate found", domain_str)
end
local gateway_nodes, err = sql_query(str_fmt(gateway_nodes_sql, tonumber(app_id)))
if not gateway_nodes then
output("failed to verify TLS certificate for app, domain: %s, err: no gateway nodes found", domain_str)
end
for _, cert_domain in ipairs(cert_domains) do
local acme_host = cert_domain.acme_host
if is_wildcard_domain(acme_host) and app_domains_hash[acme_host] then
err_tbl[#err_tbl + 1] = "wildcard app with wildcard cert is not supported yet: " .. tostring(acme_host)
goto _next_
end
if is_wildcard_domain(acme_host) then
for _, app_domain in ipairs(app_domains) do
local domain = app_domain.domain
local base_acme_host = substr(acme_host, 2)
if re_find(domain, [[\A(?:\Q\E.*?\Q]] .. base_acme_host .. [[\E)]], 'josm') then
check_list[#check_list + 1] = app_domains_hash[app_domain]
end
end
goto _next_
end
local hit = false
for _, app_domain in ipairs(app_domains) do
local domain = app_domain.domain
if domain == acme_host then
check_list[#check_list + 1] = app_domains_hash[acme_host]
goto _next_
end
if is_wildcard_domain(domain) then
local base_domain = substr(domain, 2)
if re_find(acme_host, [[\A(?:\Q\E.*?\Q]] .. base_domain .. [[\E)]], 'josm') then
local app_obj = app_domains_hash[domain]
check_list[#check_list + 1] = {
domain = acme_host,
is_wildcard = app_obj.is_wildcard,
https_ports = app_obj.https_ports
}
hit = true
end
end
end
if hit then
goto _next_
end
err_tbl[#err_tbl + 1] = str_fmt(
"certificate with Common Name '%s' not found matched host in current application '%s'",
acme_host, domain_str)
::_next_::
end
local check_domain_list = {}
for _, check_obj in pairs(check_list) do
local check_domain = check_obj.domain
local https_ports = check_obj.https_ports
check_domain_list[#check_domain_list + 1] = check_domain
for _, gateway_node in ipairs(gateway_nodes) do
local ip = gateway_node.external_ip or gateway_node.external_ipv6
if not ip then
goto _next_node_
end
for _, https_port in ipairs(https_ports) do
local ok, err = ssl_handshake(ip, https_port, check_domain)
if not ok then
err_tbl[#err_tbl + 1] = str_fmt("domain '%s' on ip '%s': '%s'",
check_domain, ip, err)
goto _next_node_
end
ok_tbl[#ok_tbl + 1] = str_fmt("domain '%s' on ip '%s'", check_domain, ip)
end
::_next_node_::
end
end
if #err_tbl == 0 then
output("OK: all TLS certificates are verified: " .. table.concat(check_domain_list, ','))
else
output("ERR: following TLS certificates are failed: " .. table.concat(err_tbl, "\n === \n"))
if #ok_tbl > 0 then
output("INFO: following TLS certificates are verified: " .. table.concat(ok_tbl, "\n === \n"))
else
output("ERR: no TLS certificate is verified successfully")
end
end