Sunday, February 11, 2018

HAProxy & Lua: How to send email

Sometimes, its useful to send emails where HAProxy events occurs. Sadly, existing libraries are a sendmail wrapper or uses LuaSocket package which is incompatible with HAProxy. The SMTP protocol is very easy to understand. Note that the "S" of SMTP means "Simple" ;-).

This article proposes a simple function which use HAProxy cosocket for sending email throught SMTP.

First the main function. This function works only when it is executed inside HAProxy because it uses HAProxy cosocket. The implementation of the SMTP protocol is very limited, this function just send emails. It not support SSL, Authentication, StartTLS nor other SMTP functions. It is designed for communicating with a local or trusted SMTP server.
-- smtp : send SMTP message
-- 
-- Copyright 2018 Thierry Fournier 
--
-- This lib is compliant with HAProxy cosockets
--
function smtp_send_email(server, port, domain, from, to, data)

        local ret
        local reason
        local tcp = core.tcp()
        local smtp_wait_code = function(tcp, code)
                local ret
                -- Read headers until we reac a 2.. code.
                while true do
                        -- read line
                        ret = tcp:receive("*l")
                        if ret == nil then
                                return false, "Connection unexpectly closed"
                        end
                        -- expected code
                        if string.match(ret, code) ~= nil then
                                return true, nil
                        end
                        -- other code
                        if string.match(ret, '^%d%d%d ') ~= nil then
                                return false, ret
                        end
                        -- other informational message, wait.
                end
        end

        if tcp:connect(server, port) == nil then
                return false, "Can't connect to \""..server..":"..port.."\""
        end

        ret, reason = smtp_wait_code(tcp, '^220 ')
        if ret == false then
                tcp:close()
                return false, reason
        end

        if tcp:send("EHLO " .. domain .. "\r\n") == nil then
                tcp:close()
                return false, "Connection unexpectly closed"
        end

        ret, reason = smtp_wait_code(tcp, '^250 ')
        if ret == false then
                tcp:close()
                return false, reason
        end

        if tcp:send("MAIL FROM: <" .. from .. ">\r\n") == nil then
                tcp:close()
                return false, "Connection unexpectly closed"
        end

        ret, reason = smtp_wait_code(tcp, '^250 ')
        if ret == false then
                tcp:close()
                return false, reason
        end

        if tcp:send("RCPT TO: <" .. to .. ">\r\n") == nil then
                tcp:close()
                return false, "Connection unexpectly closed"
        end

        ret, reason = smtp_wait_code(tcp, '^250 ')
        if ret == false then
                tcp:close()
                return false, reason
        end

        if tcp:send("DATA\r\n") == nil then
                tcp:close()
                return false, "Connection unexpectly closed"
        end

        ret, reason = smtp_wait_code(tcp, '^354 ')
        if ret == false then
                tcp:close()
                return false, reason
        end

        if tcp:send(data .. "\r\n.\r\n") == nil then
                tcp:close()
                return false, "Connection unexpectly closed"
        end

        ret, reason = smtp_wait_code(tcp, '^250 ')
        if ret == false then
                tcp:close()
                return false, reason
        end

        if tcp:send("QUIT\r\n") == nil then
                tcp:close()
                return false, "Connection unexpectly closed"
        end

        ret, reason = smtp_wait_code(tcp, '^221 ')
        if ret == false then
                tcp:close()
                return false, reason
        end

        tcp:close()
        return true, nil
end
And now an example of the usage. The use case is simple: we want to send email on HAProxy event throught an action. If the sending fails, send a log. We register a new Lua action.
core.register_action("send_email", { "tcp-req", "http-req", "tcp-res", "http-res" }, function(txn)
        local ret
        local reason
        local server = "127.0.0.1"
        local port = 25
        local domain = "arpalert.org"
        local from = "haproxy-node1@arpalert.org"
        local to = "admin@arpalert.org"
        local msg = "From: " .. from .. "\r\n" ..
                    "To: " .. to .. "\r\n" ..
                    "Subject: test - " .. os.date() .. "\r\n" ..
                    "\r\n" ..
                    "test - " .. os.date() .. "\r\n"
        ret, reason = smtp_send_email(server, port, domain, from, to, msg);
        if ret == false then
                txn:Warning("Can't send email: " .. reason)
        end
end)
And the asoiated HAProxy configuration example:
global
   lua-load samples.lua
   stats socket /tmp/haproxy.sock mode 644 level admin
   tune.ssl.default-dh-param 2048

defaults
   timeout client 1m
   timeout server 1m

listen sample3
   mode http
   bind *:10030
   http-request lua.send_email
   http-request redirect location /ok

No comments:

Post a Comment