Search code examples
validationluahaproxy

HAproxy+Lua: Return requests if validation fails from Lua script


We are trying to build an incoming request validation platform using HAProxy+Lua. Our use-case is to create a LUA scripts that will essentially make a socket call to a Validation API, and based on the response from Validation API we want to redirect the request to a backend API, and if the validation fails we would want to return the request right from the LUA script. For example, for 200 response we would want to redirect the request to backend api, and for 404 we would want to return the request. From the documentation, I understand that there are various default functions available with Lua-Haproxy integration.

core.register_action() --> I'm using this. Take TXN as input
core.register_converters() --> Essentially used for string manipulations.
core.register_fetches() --> Takes TXN as input and returns string; Mainly used for representing dynamic backend profiles in haproxy config
core.register_init() --> Used for initialization
core.register_service() --> You have to return the response mandatorily while using this function, which doesn't satisfy our requirements
core.register_task() -->  For using normal functions. No mandatory input class. TXN is required to fetch header details from request

I have tried all of the functions from above list, I understand that core.register_service is basically to return a response from the Lua script. However, what is problematic is, we must send the response from the LUA script and it will not redirect the request to BACKEND. Currently, I am using core.register_action to interrupt the requests, but I'm not able to return the request using this function. Here's what my code looks like:

local http_socket = require("socket.http")
local pretty_print = require("pl.pretty")

function add_http_request_header(txn, name, value)
    local headerName = name
    local headerValue = value
    txn.http:req_add_header(headerName, headerValue)
end

function call_validation_api()
    local request, code, header = http_socket.request {
                                   method = "GET",                    -- Validation API Method                           
                                   url = "http://www.google.com/"     -- Validation API URL
                                   }

   -- Using core.log; Print in some cases is a blocking operation http://www.arpalert.org/haproxy-lua.html#h203
   core.Info( "Validation API Response Code: " .. code )
   pretty_print.dump( header )
   return code
end

function failure_response(txn)
    local response = "Validation Failed"
    core.Info(response)
    txn.res:send(response)
--    txn:close()
end

core.register_action("validation_action", { "http-req", "http-res" }, function(txn)
   local validation_api_code = call_validation_api()
   if validation_api_code == 200 then
      core.Info("Validation Successful")
      add_http_request_header(txn, "test-header", "abcdefg")
      pretty_print.dump( txn.http:req_get_headers() )
   else
      failure_response(txn) --->>> **HERE I WANT TO RETURN THE RESPONSE**
   end
end)

Following is the configuration file entry:

frontend  http-in
    bind :8000
    mode http
    http-request    lua.validation_action

    #Capturing header of the incoming request
    capture request header test-header len 64

    #use_backend %[lua.fetch_req_params]
    default_backend app

backend         app
    balance     roundrobin
    server      app1    127.0.0.1:9999  check

Any help is much appreciated in achieving this functionality. Also, I understand that SOCKET call from Lua script is a blocking call, which is opposite to HAProxy's default nature of keep-alive connection. Please feel free to suggest any other utility to achieve this functionality, if you have already used it.


Solution

  • Ok I have figured out the answer to this question: I created 2 backends for success and failure of requests, and based on the response I am returning 2 different strings. In "failure_backend", I have called a different service, which essentially is a core.register_service and can return the response. I'm pasting code for both the configuration file and lua script

    HAProxy conf file:

    #---------------------------------------------------------------------
    # Global settings
    #---------------------------------------------------------------------
    global
        # to have these messages end up in /var/log/haproxy.log you will
        # need to:
        #
        # 1) configure syslog to accept network log events.  This is done
        #    by adding the '-r' option to the SYSLOGD_OPTIONS in
        #    /etc/sysconfig/syslog
        #
        # 2) configure local2 events to go to the /var/log/haproxy.log
        #   file. A line like the following can be added to
        #   /etc/sysconfig/syslog
        #
        #    local2.*                       /var/log/haproxy.log
        #
        log         127.0.0.1 local2
        maxconn     4000
        user        haproxy
        group       haproxy
        daemon
        #lua file load
        lua-load    /home/aman/coding/haproxy/http_header.lua
    
        # turn on stats unix socket
        stats       socket      /var/lib/haproxy/stats
    
    #---------------------------------------------------------------------
    # common defaults that all the 'listen' and 'backend' sections will
    # use if not designated in their block
    #---------------------------------------------------------------------
    defaults
        mode        http
        log         global
        option      httplog
        option      dontlognull
        retries     3
        timeout     http-request    90s
        timeout     queue           1m
        timeout     connect         10s
        timeout     client          1m
        timeout     server          1m
        timeout     http-keep-alive 10s
        timeout     check           10s
        maxconn     3000
    
    #---------------------------------------------------------------------
    # main frontend which proxys to the backends
    #---------------------------------------------------------------------
    frontend            http-in
        bind            :8000
        mode            http
        use_backend     %[lua.validation_fetch]
        default_backend failure_backend
    
    #---------------------------------------------------------------------
    # round robin balancing between the various backends
    #---------------------------------------------------------------------
    backend         success_backend
        balance     roundrobin
        server      app1    172.23.12.94:9999  check
    
    backend             failure_backend
        http-request    use-service     lua.failure_service
    
    # For displaying HAProxy statistics.
    frontend            stats
        bind            :8888
        default_backend stats
    
    backend     stats
        stats   enable
        stats   hide-version
        stats   realm Haproxy Statistics
        stats   uri /haproxy/stats
        stats   auth aman:rjil@123
    

    Lua script:

    local http_socket = require("socket.http")
    local pretty_print = require("pl.pretty")
    
    function add_http_request_header(txn, name, value)
        local headerName = name
        local headerValue = value
        txn.http:req_add_header(headerName, headerValue)
    end
    
    function call_validation_api()
        local request, code, header = http_socket.request {
                                       method = "GET",                          -- Validation API Method                           
                                       url = "http://www.google.com/"     -- Validation API URL
                                       }
    
       -- Using core.log; Print in some cases is a blocking operation http://www.arpalert.org/haproxy-lua.html#h203
       core.Info( "Validation API Response Code: " .. code )
       pretty_print.dump( header )
       return code
    end
    
    function failure_response(txn)
        local response = "Validation Failed"
        core.Info(response)
        return "failure_backend"
    end
    
    -- Decides back-end based on Success and Failure received from validation API
    core.register_fetches("validation_fetch", function(txn)
       local validation_api_code = call_validation_api()
       if validation_api_code == 200 then
          core.Info("Validation Successful")
          add_http_request_header(txn, "test_header", "abcdefg")
          pretty_print.dump( txn.http:req_get_headers() )
          return "success_backend"
       else
          failure_response(txn)
       end
    end)
    
    -- Failure service
    core.register_service("failure_service", "http", function(applet)
          local response = "Validation Failed"
          core.Info(response)
          applet:set_status(400)
          applet:add_header("content-length", string.len(response))
          applet:add_header("content-type", "text/plain")
          applet:start_response()
          applet:send(response)
    end)