aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSergey Poznyakoff <gray@gnu.org>2021-03-05 16:11:15 +0200
committerSergey Poznyakoff <gray@gnu.org>2021-03-07 16:21:54 +0200
commit865ff3a51306c4dd789364a1463000a182f19c97 (patch)
tree814661f35d9f3a378fd87b1556b6b694b7ade32c
downloadhaproxy-bulkredirect-865ff3a51306c4dd789364a1463000a182f19c97.tar.gz
haproxy-bulkredirect-865ff3a51306c4dd789364a1463000a182f19c97.tar.bz2
Initial commit
-rw-r--r--bulkredirect.lua445
1 files changed, 445 insertions, 0 deletions
diff --git a/bulkredirect.lua b/bulkredirect.lua
new file mode 100644
index 0000000..20666ca
--- /dev/null
+++ b/bulkredirect.lua
@@ -0,0 +1,445 @@
+-- Bulk redirects for HAProxy
+-- Copyright (C) 2021 Sergey Poznyakoff
+--
+-- This program is free software; you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation; either version 3, or (at your option)
+-- any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+local bulkredirect = {}
+__ENV = bulkredirect
+
+--[[
+ Iterator factory to successively strip off the trailing pathname
+ element from a path.
+
+ On input, T is the pathname (optionally terminated by /).
+ Returns iterator function that, on each call, returns the pathname
+ with its trailing element removed. Trailing element here means the
+ everything past the last '/' character.
+]]
+local function prevsegm (t)
+ local s = t
+ if not s:match("/$") then s = s .. "/" end
+ return function ()
+ s = s:match("(.*)/")
+ return s
+ end
+end
+
+--[[
+ Module global variables:
+
+ rt
+ Redirection table. It is indexed by the request host name
+ (the value given by the HTTP Host header). The corresponding value
+ can be either a string or a table.
+
+ A string value refers to another host name (an alias).
+
+ A table supplies the actual mapping from an URI to another URL.
+ When a request to P?Q arrives (where P denotes the pathname part and
+ Q denotes query arguments), the P is looked up in the table. If the
+ entry rt[P] does not exist, the trailing pathname component is
+ removed from P and the resulting string P1 (unless it is empty) is
+ used as the look-up key. The process continues until either the entry
+ rt[P1] is found, or P1 is reduced to an empty string. The latter means
+ there is no redirect for the given P?Q combination.
+
+ The lookup process is reduced to a single look up if the global
+ variable 'exact' is true (see below).
+
+ If the entry rt[P1] is found and is a string, it gives the location
+ of the redirect. Prior to returning a 301 reply, this location is
+ augmented by pathname part removed from P during the look up process,
+ and the quiery part (if any). These modifications are controlled by
+ the global variables 'strippath' and 'stripquery', described below.
+ If 'strippath' is true, the removed pathname components are not added
+ back to the location. Similarly, if 'stripquery' is true, the query
+ part is not appended to the location. The 301 reply code is the
+ default. If the 'temporary' global variable is set to true, the code
+ 302 will be used instead.
+
+ If rt[P1] is a table, it must contain a sequence of two elements.
+ The element rt[P1][1] supplies the new location if Q is not present.
+ The element rt[P1][2] is an associative table, indexed by possible
+ values of Q (including empty value). Both rt[P1][1] and rt[P1][2][X]
+ can contain either a string or a table value. The string value
+ supplies the new location as described above. The table value
+ supplies the new location in the element [1]. Rest of elements
+ (2 up to 5) are boolean values overriding the default global variables
+ for this entries. They are located in the following order:
+
+ 2 - exact
+ 3 - strippath
+ 4 - stripquery
+ 5 - temporary
+
+ www
+ If set to true, the rules for a hostname "X" apply also to
+ hostname "www.X"
+
+ exact
+ If set to true, no path prefix search is done: the redirection reply
+ is returned only if the input path is present in the table.
+
+ strippath
+ By default the path components stripped during the look up process
+ are added back to the returned location. If this variable is set
+ to true, these components are dropped instead.
+
+ stripquery
+ By default the query part (if any) is appended to the new
+ location when returning the redirection reply. Setting this variable
+ to true disables this
+
+ temporary
+ Whether to return temporary (302) or permanent (301) reply.
+]]
+
+--
+-- Redirect the request if it matches one of the entries in the RT table.
+--
+function bulkredirect.request (txn)
+ local headers = txn.http:req_get_headers()
+ local reply = txn:reply()
+ local path = txn.f:path():sub(2)
+ local host = headers["host"][0]
+
+ -- Get the per-host redirection table
+ local rthost = rt[host]
+
+ -- Resolve eventual alias chain.
+ while type(rthost) == 'string'
+ do
+ rthost = rt[rthost]
+ end
+
+ -- If no corresponding host entry found, start from the default
+ -- entry.
+ if not rthost then
+ rthost = rt["*"]
+ end
+
+ if rthost then
+ local location
+
+ -- Successively strip the trailing element off the path and look up
+ -- the remaining part in the table.
+ for i in prevsegm(path) do
+ local exact, strippath, stripquery, temporary = exact, strippath, stripquery, temporary
+
+ if rthost[i] then
+ -- If the entry is found, it is either a table or a string
+ if type(rthost[i]) == 'table' then
+ local dt = rthost[i]
+ local query = txn.f:query()
+ if query then
+ if dt[2] then
+ dt = dt[2][query]
+ else
+ dt = dt[1]
+ end
+ else
+ dt = dt[1]
+ end
+
+ location = dt[1]
+ if dt[2] ~= nil then
+ exact = dt[2]
+ end
+ if dt[3] ~= nil then
+ strippath = dt[3]
+ end
+ if dt[4] ~= nil then
+ stripquery = dt[4]
+ end
+ if dt[5] ~= nil then
+ temporary = dt[5]
+ end
+ else
+ location = rthost[i]
+ end
+ end
+
+ if location then
+ if not exact or i == path or i..'/' == path then
+ if not strippath then
+ location = location .. path:sub(i:len() + 1)
+ end
+ if not stripquery and txn.f:query() then
+ location = location .. '?' .. txn.f:query()
+ end
+
+ core.Debug("REDIRECT " .. host .. txn.f:path() .. " to " .. location)
+ if temporary then
+ reply:set_status(302, "Moved Temporarily")
+ else
+ reply:set_status(301, "Moved Permanently")
+ end
+ reply:add_header("Location", location)
+ txn:done(reply)
+ break
+ end
+ end
+ end
+ end
+end
+
+-- Populate the redirection table
+--[[
+ Syntax:
+
+ input ::= statement | input statement
+ statement ::= option | domain | redirect
+ option ::= 'option' optlist [,]
+ domain ::= '[' HOSTNAME ']'
+ redirect ::= STRING STRING optlist [,]
+ optlist ::= OPTNAME | optilist ',' OPTNAME
+ OPTNAME ::= 'www' | 'exact' | 'strippath' | 'stripquery' | 'temporary'
+ <or any of these prefixed with 'no'>
+ HOSTNAME ::= <any valid hostname>
+]]
+
+local function parseopt (s, t, loc)
+ local valid = {
+ ['www'] = true,
+ ['exact'] = true,
+ ['strippath'] = true,
+ ['stripquery'] = true,
+ ['temporary'] = true
+ }
+
+ function options (str, loc)
+ local s = str
+ return function ()
+ if s == nil or s == "" then
+ return nil
+ end
+
+ local opt, rest
+ opt, rest = s:match("^(%w+)%s*,%s*(.*)")
+ if not opt then
+ opt = s:match("^%w+$")
+ if not opt then
+ error(loc .. ": bad option list syntax near " .. s, 0)
+ end
+ end
+ s = rest
+ return opt
+ end
+ end
+
+ if not t then
+ t = _ENV
+ end
+
+ for opt in options(s, loc) do
+ local neg = opt:match("^no(%w+)$")
+ local val = true
+
+ if neg then
+ opt = neg
+ val = not val
+ end
+ if not valid[opt] then
+ error(loc .. ': invalid option ' .. opt, 0)
+ end
+
+ t[opt] = val
+ end
+end
+
+local function set_dst (dt, src, dst)
+ local path, query = src:match('^(.+)?(.+)')
+ if path then
+ if not dt[src] then
+ dt[src] = {nil, {}}
+ elseif type(dt[src]) == 'string' then
+ dt[src] = { { dt[src] }, {} }
+ end
+ dt[src][2][query] = dst
+ elseif type(dt[src]) == 'table' then
+ dt[src][1] = dst
+ elseif type(dst) == 'table' then
+ dt[src] = { dst }
+ else
+ dt[src] = dst
+ end
+end
+
+local function clone (orig)
+ local copy
+ if type(orig) == 'table' then
+ copy = {}
+ for k,v in pairs(orig) do
+ copy[k] = clone(v)
+ end
+ setmetatable(copy, clone(getmetatable(orig)))
+ else
+ copy = orig
+ end
+ return copy
+end
+
+local function www_complement (name)
+ local s = name:match('^www%.(.+)')
+ if not s then s = 'www.' .. name end
+ return s
+end
+
+local function load_redirect_file (f, filename)
+ local domain
+ local ln = 1
+
+ local rt = {}
+ local domopt = {}
+
+ local parsetab = {
+ { '^#', function () end },
+ { '^%s*$', function () end },
+ { '^option%s+(.*)', function (s)
+ local t
+ if domain then
+ t = domopt
+ else
+ t = _ENV
+ end
+ parseopt(s, t, filename .. ':' .. ln)
+ end },
+ { '^%s*%[(.+)%]%s*$', function (s)
+ domain = s
+ if not rt[domain] then rt[domain] = {} end
+ end },
+ { '^%s*([^%s]+)%s+([^%s]+)%s*(.*)$', function (src, dst, optlist)
+ if not domain then
+ error(filename .. ':' .. ln .. ': declare [domain] first', 0)
+ end
+
+ if src:match('^/') then
+ src = src:sub(2)
+ end
+
+ if dst:match('^/') then
+ dst = dst:sub(2)
+ end
+
+ local optab = domopt
+ if optlist ~= '' then
+ optab = clone(domopt)
+ parseopt(optlist, optab, filename .. ':' .. ln)
+ end
+
+ if optab['exact'] then
+ if type(dst) == 'string' then dst = { dst } end
+ dst[2] = optab['exact']
+ end
+ if optab['strippath'] then
+ if type(dst) == 'string' then dst = { dst } end
+ dst[3] = optab['strippath']
+ end
+ if optab['stripquery'] then
+ if type(dst) == 'string' then dst = { dst } end
+ dst[4] = optab['stripquery']
+ end
+ if optab['temporary'] then
+ if type(dst) == 'string' then dst = { dst } end
+ dst[5] = optab['temporary']
+ end
+
+ if optab['www'] ~= nil then
+ if www == optab['www'] then
+ return
+ elseif www then
+ for d,t in pairs(rt) do
+ local compl = www_complement(d)
+ if rt[compl] then
+ for k,v in pairs(t) do
+ -- FIXME: Error message if rt[compl][k] exists
+ rt[compl][k] = clone(v)
+ end
+ else
+ rt[compl] = clone(t)
+ end
+ end
+ www = nil
+ end
+ end
+
+ set_dst(rt[domain], src, dst)
+
+ if optab['www'] then
+ local s = www_complement(domain)
+ if not rt[s] then rt[s] = {} end
+ set_dst(rt[s], src, dst)
+ end
+ end }
+ }
+
+ for line in f:lines() do
+ ln = ln + 1
+ for i = 1, #parsetab do
+ local t = {line:match(parsetab[i][1])}
+ if t[1] then
+ parsetab[i][2](table.unpack(t))
+ goto continue
+ end
+ end
+
+ error(filename .. ':' .. ln .. ': syntax error', 0)
+ ::continue::
+ end
+
+ if www then
+ for d,t in pairs(rt) do
+ local compl = www_complement(d)
+ if rt[compl] then
+ for k,v in pairs(t) do
+ -- FIXME: Error message if rt[compl][k] exists
+ rt[compl][k] = clone(v)
+ end
+ else
+ rt[compl] = d
+ end
+ end
+ www = nil
+ end
+
+ _ENV['rt'] = rt
+end
+
+local function load_redirect_table ()
+ local name = os.getenv('HAPROXY_BULKREDIRECT')
+ if name == nil then
+ name = '/etc/haproxy/bulkredirect.lua'
+ end
+ if name:match('%.lua$') then
+ rt, www, exact, strippath, stripquery, temporary = dofile(name)
+ else
+ local file, err = io.open(name,"r")
+ if file ~= nil then
+ local status, err = pcall(load_redirect_file, file, name)
+ file:close()
+ if not status then
+ core.Alert(err)
+ end
+ else
+ core.Alert("can't open " .. name .. ": " .. err)
+ end
+ end
+end
+
+-- Load redirects
+load_redirect_table()
+
+-- Register the actions with HAProxy
+core.register_action("bulkredirect", {"http-req"}, bulkredirect.request, 0)
+

Return to:

Send suggestions and report system problems to the System administrator.