Sunday, February 11, 2018

HAProxy & Lua: How to use sample-fetches

It's not easy to understand usage of Lua in HAProxy. The best way is a cpy paste of samples. This page will collect some samples of basic usages.
Note that this tuto is not:
  • HAProxy configuration tutorial. The reader must known HAProxy basics
  • Lua programation tutorial. The reader must known Lua basics

Useful links

Before start

In the following examples, I use the function print_r(). This function is usefull for displaying complex Lua structures. It is used for debugging. You can found this function in the page Lua scripts. The samples.lua file starts with an include (require()) of the print_r script.

require("print_r")

For testing the following samples, we always use HAProxy in debug mode. The global section of the configuration file is like the following. This is not a production ready configuration !

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

The configuration file is named haproxy.cfg, all the Lua samples which could be copy, need to be pasted in the file samples.lua. The command line for starting sample is like the following. We assume a start from the directory containing the two configuration file (for default path reasons).
haproxy -d -f haproxy.cfg
Most of tests are performed whith Curl.

The documentation links points to the HAProxy version 1.7 documentation.

Note about sample-fetches wrappers and Lua Notation: txn.f is an array of wrapper functions. The object like members of an array doesn't supports the "." which is a reserved character. So all the sample fetches are replaced by a "_".

With Lua, a function is a variable. For convenience, the executed function can be declared inline as anonymous functions or referenced as named function. So the two sample below have the same behavior.
-- Named function
function a(arg)
   print("hello")
end
register_callback(a)

-- Inline function
register_callback(function(arg)
   print("hello")
end)
I will use the inline notation. Bellow the first sample commented. Note that all the Lua registered function are mapped in the HAProxy configuration with a prefix "lua.".

Using HAProxy sample fetches inside Lua

This first example is absolutely useless. It just shows how to create a Lua sample-fetch which return the same content that an embedded sample-fetch. By the way, it shows how to use embedded sample fetches in Lua.
First, HAProxy provides some functions and class. Some of these ones are availaible from the start of HAProxy. Where the lua file is read, it is executed, so the Lua file is executed during the start of HAProxy.
New all the functions which will be call by HAProxy (called "hook") needs to be registered. For declaring a Lua sample-fetch we will use the function core.register_fetches()
  • core.register_fetches(): Function (doc): Register Lua sample-fetch. register_fetches takes a function as argument. This function will be called according with the HAProxy
  • txn: Class TXN (doc): Provides functions available with the transaction.
  • txn.f: Class Fetches (doc): Provides an access to all the haproxy sample fetches. There are called sample-fetches wrappers.

-- This register new sample-fetch called "lua.sample1"
core.register_fetches("sample1", function(txn)

   -- By default, all varibles are global. specify the local
   -- keyword to avoid bug. The following line store the 
   -- content of the sample-fetch "path" in the variable
   -- "path".
   local path = txn.f:path()

   -- Print out the content of path variable. The data
   -- id display on the HAProxy standard output.
   print_r(path)

   -- Return the value of the path as the sample-fetch
   -- result.
   return txn.f:path()
end)
We can use this new sample-fetch like the embedded sample-fetch"path":
listen sample1
   mode http
   bind *:10010
   acl use-lua lua.sample1 -m beg /use-lua
   acl use-php lua.sample1 -m beg /use-php
   http-request redirect location /lua if use-lua
   http-request redirect location /php if use-php
   http-request redirect location /other
Start HAProxy and test with curl:
$ curl -s -i http://127.0.0.1:10010/use-lua | grep Location
Location: /lua

$ curl -s -i http://127.0.0.1:10010/use-php | grep Location
Location: /php

$ curl -s -i http://127.0.0.1:10010/use-other | grep Location
Location: /other

Simplify complex conditions

In some cases, configurations have complex conditions based on sample-fetches. These kind of conditions are not easy to maintain because there are not easily understandable. We are quickly lost with a lot of ACL. A prod likely example is choosing redirect to https or no according with some conditions. The conditions are prod or preprod, already ssl or no, protected page or no. We want to force https for the payment page, except for the preprod, except if the request is already SSL and except if the DEBUG cookie is set.
We want to redirect to http if the request is ssl and if the preprod is required or if not a payment page is required and if the exception cookie is not set.
These condition are a little bit complex with classic ACL. The Lua can help us. The foolowing table resule input name, their definition and the testing method:
Condition name Description Test with Curl
is_payment_page The payment page is requested curl http://127.0.0.1:10020/payment/
is_ssl The request is performed using SSL curl -k https://127.0.0.1:10021/
is_preprod The preprod is required curl http://127.0.0.1:10020/ -H 'Host: preprod.test.com'
is_cookie_exception A DEBUG cookie is set curl http://127.0.0.1:10020/ -H 'Cookie: DEBUG=1'
Below the truth table resuming actions:
is_paymentis_sslis_preprodis_debugaction
0000forward
0001forward
0010forward
0011forward
0100HTTP redirect
0101HTTP redirect
0110HTTP redirect
0111HTTP redirect
1000HTTPS redirect
1001forward
1010forward
1011forward
1100forward
1101HTTP redirect
1110HTTP redirect
1111HTTP redirect
Below the Lua code performing conditions. The code is split in two parts. The first part extract inputs, and the second part perform conditions based on inputs.
-- This function returns all condition in an array.
function get_variables(txn)

   -- This array will contains conditions
   local cond = {}

   -- True if the path starts with "/payment/"
   cond['is_payment_page'] = string.match(txn.sf:path(), '^/payment/') ~= nil

   -- True if the front connection is SSL
   cond['is_ssl'] = txn.f:ssl_fc() == 1

   -- True if the domain name asked is preprod
   cond['is_preprod'] = txn.f:req_fhdr('host') == 'preprod.test.com'

   -- True if the cookie 'DEBUG' is set
   cond['is_cookie_exception'] = txn.f:req_cook_cnt('DEBUG') >= 1

   -- Display extracted conditions
   -- print_r(cond)
   return cond
end

-- This sample fetch return 1 if we need HTTPS redirect
core.register_fetches("sample2_1", function(txn)

   -- Get input conditions
   local cond = get_variables(txn)

   -- Return result according with conditions value and policy.
   if cond['is_ssl']              then return 0 end
   if cond['is_cookie_exception'] then return 0 end
   if cond['is_preprod']          then return 0 end
   if cond['is_payment_page']     then return 1 end
   return 0
end)

-- This sample fetch returns 1 if we need HTTP redirect
core.register_fetches("sample2_2", function(txn)

   -- Get input conditions
   local cond = get_variables(txn)

   -- Return result according with conditions value and policy.
   if not cond['is_ssl']          then return 0 end
   if cond['is_cookie_exception'] then return 1 end
   if cond['is_preprod']          then return 1 end
   if not cond['is_payment_page'] then return 1 end
   return 0
end)
And the HAProxy corresponding code:
listen sample2
   mode http
   bind *:10020
   bind *:10021 ssl crt www.test.com.crt crt preprod.test.com.crt
   http-request redirect location /to-https if { lua.sample2_1 1 }
   http-request redirect location /to-http  if { lua.sample2_2 1 }
   http-request redirect location /forward

No comments:

Post a Comment