Diff

mod_pubsub_forgejo/mod_pubsub_forgejo.lua @ 6203:131b8bfbefb4

mod_pubsub_forgejo: new module for forgejo webhooks
author nicoco <nicoco@nicoco.fr>
date Mon, 17 Feb 2025 23:28:05 +0100
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_pubsub_forgejo/mod_pubsub_forgejo.lua	Mon Feb 17 23:28:05 2025 +0100
@@ -0,0 +1,115 @@
+module:depends("http")
+local pubsub_service = module:depends("pubsub").service
+
+local st = require "util.stanza"
+local json = require "util.json"
+local hashes = require "util.hashes"
+local from_hex = require"util.hex".from
+local hmacs = {
+	sha1 = hashes.hmac_sha1,
+	sha256 = hashes.hmac_sha256,
+	sha384 = hashes.hmac_sha384,
+	sha512 = hashes.hmac_sha512
+}
+
+local format = module:require "format"
+local default_templates = module:require "templates"
+
+-- configuration
+local forgejo_secret = module:get_option("forgejo_secret")
+
+local default_node = module:get_option("forgejo_node", "forgejo")
+local node_prefix = module:get_option_string("forgejo_node_prefix", "forgejo/")
+local node_mapping = module:get_option_string("forgejo_node_mapping")
+local forgejo_actor = module:get_option_string("forgejo_actor") or true
+
+local skip_commitless_push = module:get_option_boolean(
+				                             "forgejo_skip_commitless_push", true)
+local custom_templates = module:get_option("forgejo_templates")
+
+local forgejo_templates = default_templates
+
+if custom_templates ~= nil then
+	for k, v in pairs(custom_templates) do forgejo_templates[k] = v end
+end
+
+-- used for develoment, should never be set in prod!
+local insecure = module:get_option_boolean("forgejo_insecure", false)
+-- validation
+if not insecure then assert(forgejo_secret, "Please set 'forgejo_secret'") end
+
+local error_mapping = {
+	["forbidden"] = 403,
+	["item-not-found"] = 404,
+	["internal-server-error"] = 500,
+	["conflict"] = 409
+}
+
+local function verify_signature(secret, body, signature)
+	if insecure then return true end
+	if not signature then return false end
+	local algo, digest = signature:match("^([^=]+)=(%x+)")
+	if not algo then return false end
+	local hmac = hmacs[algo]
+	if not algo then return false end
+	return hmac(secret, body) == from_hex(digest)
+end
+
+function handle_POST(event)
+	local request, response = event.request, event.response
+
+	if not verify_signature(forgejo_secret, request.body,
+	                        request.headers.x_hub_signature) then
+		module:log("debug", "Signature validation failed")
+		return 401
+	end
+
+	local data = json.decode(request.body)
+	if not data then
+		response.status_code = 400
+		return "Invalid JSON. From you of all people..."
+	end
+
+	local forgejo_event = request.headers.x_forgejo_event or data.object_kind
+
+	if skip_commitless_push and forgejo_event == "push" and data.total_commits == 0 then
+		module:log("debug", "Skipping push event with 0 commits")
+		return 501
+	end
+
+	if forgejo_templates[forgejo_event] == nil then
+		module:log("debug", "Unsupported forgejo event %q", forgejo_event)
+		return 501
+	end
+
+	local item = format(data, forgejo_templates[forgejo_event])
+
+	if item == nil then
+		module:log("debug", "Formatter returned nil for event %q", forgejo_event)
+		return 501
+	end
+
+	local node = default_node
+	if node_mapping then node = node_prefix .. data.repository[node_mapping] end
+
+	create_node(node)
+
+	local ok, err = pubsub_service:publish(node, forgejo_actor, item.attr.id, item)
+	if not ok then return error_mapping[err] or 500 end
+
+	response.status_code = 202
+	return "Thank you forgejo.\n" .. tostring(item:indent(1, " "))
+end
+
+module:provides("http", {route = {POST = handle_POST}})
+
+function create_node(node)
+	if not pubsub_service.nodes[node] then
+		local ok, err = pubsub_service:create(node, true)
+		if not ok then
+			module:log("error", "Error creating node: %s", err)
+		else
+			module:log("debug", "Node %q created", node)
+		end
+	end
+end