212 lines
7.4 KiB
Lua
212 lines
7.4 KiB
Lua
-- The MIT License (MIT)
|
|
--
|
|
-- Copyright (c) 2018 Tim Düsterhus
|
|
--
|
|
-- Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
-- of this software and associated documentation files (the "Software"), to deal
|
|
-- in the Software without restriction, including without limitation the rights
|
|
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
-- copies of the Software, and to permit persons to whom the Software is
|
|
-- furnished to do so, subject to the following conditions:
|
|
--
|
|
-- The above copyright notice and this permission notice shall be included in all
|
|
-- copies or substantial portions of the Software.
|
|
--
|
|
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
-- SOFTWARE.
|
|
--
|
|
-- SPDX-License-Identifier: MIT
|
|
|
|
local http = require("haproxy-lua-http")
|
|
|
|
core.register_action("auth-request", { "http-req" }, function(txn, be, path)
|
|
auth_request(txn, be, path, "HEAD", ".*", "-", "-")
|
|
end, 2)
|
|
|
|
core.register_action("auth-intercept", { "http-req" }, function(txn, be, path, method, hdr_req, hdr_succeed, hdr_fail)
|
|
hdr_req = globToLuaPattern(hdr_req)
|
|
hdr_succeed = globToLuaPattern(hdr_succeed)
|
|
hdr_fail = globToLuaPattern(hdr_fail)
|
|
auth_request(txn, be, path, method, hdr_req, hdr_succeed, hdr_fail)
|
|
end, 6)
|
|
|
|
function globToLuaPattern(glob)
|
|
if glob == "-" then
|
|
return "-"
|
|
end
|
|
-- magic chars: '^', '$', '(', ')', '%', '.', '[', ']', '*', '+', '-', '?'
|
|
-- https://www.lua.org/manual/5.4/manual.html#6.4.1
|
|
--
|
|
-- this chain is:
|
|
-- 1. escaping all the magic chars, adding a `%` in front of all of them,
|
|
-- except the chars being processed later in the chain;
|
|
-- 1.1. all the chars inside the [set] are magic chars and have special
|
|
-- meaning inside a set, so we're also escaping all of them to avoid
|
|
-- misbehavior;
|
|
-- 2. converting "match all" `*` and "match one" `?` to their Lua pattern
|
|
-- counterparts;
|
|
-- 3. adding start and finish boundaries outside the whole string and,
|
|
-- being a comma-separated list, between every single item as well.
|
|
return "^" .. glob:gsub("[%^%$%(%)%%%.%[%]%+%-]", "%%%1"):gsub("*", ".*"):gsub("?", "."):gsub(",", "$,^") .. "$"
|
|
end
|
|
|
|
function set_var_pre_2_2(txn, var, value)
|
|
return txn:set_var(var, value)
|
|
end
|
|
function set_var_post_2_2(txn, var, value)
|
|
return txn:set_var(var, value, true)
|
|
end
|
|
|
|
set_var = function(txn, var, value)
|
|
local success = pcall(set_var_post_2_2, txn, var, value)
|
|
if success then
|
|
set_var = set_var_post_2_2
|
|
else
|
|
set_var = set_var_pre_2_2
|
|
end
|
|
|
|
return set_var(txn, var, value)
|
|
end
|
|
|
|
function sanitize_header_for_variable(header)
|
|
return header:gsub("[^a-zA-Z0-9]", "_")
|
|
end
|
|
|
|
-- header_match checks whether the provided header matches the pattern.
|
|
-- pattern is a comma-separated list of Lua Patterns.
|
|
function header_match(header, pattern)
|
|
if header == "content-length" or header == "host" or pattern == "-" then
|
|
return false
|
|
end
|
|
for p in pattern:gmatch("[^,]*") do
|
|
if header:match(p) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Terminates the transaction and sends the provided response to the client.
|
|
-- hdr_fail filters header names that should be provided using Lua Patterns.
|
|
function send_response(txn, response, hdr_fail)
|
|
local reply = txn:reply()
|
|
if response then
|
|
reply:set_status(response.status_code)
|
|
for header, value in response:get_headers(false) do
|
|
if header_match(header, hdr_fail) then
|
|
reply:add_header(header, value)
|
|
end
|
|
end
|
|
if response.content then
|
|
reply:set_body(response.content)
|
|
end
|
|
else
|
|
reply:set_status(500)
|
|
end
|
|
txn:done(reply)
|
|
end
|
|
|
|
-- auth_request makes the request to the external authentication service
|
|
-- and waits for the response. hdr_* params receive a comma-separated
|
|
-- list of Lua Patterns used to identify the headers that should be
|
|
-- copied between the requests and responses. A dash `-` in these params
|
|
-- mean that the headers shouldn't be copied at all.
|
|
-- Special values and behavior:
|
|
-- * method == "*": call the auth service using the same method used by the client.
|
|
-- * hdr_fail == "-": make the Lua script to not terminate the request.
|
|
function auth_request(txn, be, path, method, hdr_req, hdr_succeed, hdr_fail)
|
|
set_var(txn, "txn.auth_response_successful", false)
|
|
|
|
-- Check whether the given backend exists.
|
|
if core.backends[be] == nil then
|
|
txn:Alert("Unknown auth-request backend '" .. be .. "'")
|
|
set_var(txn, "txn.auth_response_code", 500)
|
|
return
|
|
end
|
|
|
|
-- Check whether the given backend has servers that
|
|
-- are not `DOWN`.
|
|
local addr = nil
|
|
for name, server in pairs(core.backends[be].servers) do
|
|
local status = server:get_stats()['status']
|
|
if status == "no check" or status:find("UP") == 1 then
|
|
addr = server:get_addr()
|
|
break
|
|
end
|
|
end
|
|
if addr == nil then
|
|
txn:Warning("No servers available for auth-request backend: '" .. be .. "'")
|
|
set_var(txn, "txn.auth_response_code", 500)
|
|
return
|
|
end
|
|
|
|
-- Transform table of request headers from haproxy's to
|
|
-- socket.http's format.
|
|
local headers = {}
|
|
for header, values in pairs(txn.http:req_get_headers()) do
|
|
if header_match(header, hdr_req) then
|
|
for i, v in pairs(values) do
|
|
if headers[header] == nil then
|
|
headers[header] = v
|
|
else
|
|
headers[header] = headers[header] .. ", " .. v
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Make request to backend.
|
|
if method == "*" then
|
|
method = txn.sf:method()
|
|
end
|
|
local response, err = http.send(method:upper(), {
|
|
url = "http://" .. addr .. path,
|
|
headers = headers,
|
|
})
|
|
|
|
-- `terminate_on_failure == true` means that the Lua script should send the response
|
|
-- and terminate the transaction in the case of a failure. This will happen when
|
|
-- hdr_fail content isn't a dash `-`.
|
|
local terminate_on_failure = hdr_fail ~= "-"
|
|
|
|
-- Check whether we received a valid HTTP response.
|
|
if response == nil then
|
|
txn:Warning("Failure in auth-request backend '" .. be .. "': " .. err)
|
|
set_var(txn, "txn.auth_response_code", 500)
|
|
if terminate_on_failure then
|
|
send_response(txn)
|
|
end
|
|
return
|
|
end
|
|
|
|
set_var(txn, "txn.auth_response_code", response.status_code)
|
|
local response_ok = 200 <= response.status_code and response.status_code < 300
|
|
|
|
for header, value in response:get_headers(true) do
|
|
set_var(txn, "req.auth_response_header." .. sanitize_header_for_variable(header), value)
|
|
if response_ok and hdr_succeed ~= "-" and header_match(header, hdr_succeed) then
|
|
txn.http:req_set_header(header, value)
|
|
end
|
|
end
|
|
|
|
-- response_ok means 2xx: allow request.
|
|
if response_ok then
|
|
set_var(txn, "txn.auth_response_successful", true)
|
|
-- Don't allow codes < 200 or >= 300.
|
|
-- Forward the response to the client if required.
|
|
elseif terminate_on_failure then
|
|
send_response(txn, response, hdr_fail)
|
|
-- Codes with Location: Passthrough location at redirect.
|
|
elseif response.status_code == 301 or response.status_code == 302 or response.status_code == 303 or response.status_code == 307 or response.status_code == 308 then
|
|
set_var(txn, "txn.auth_response_location", response:get_header("location", "last"))
|
|
-- 401 / 403: Do nothing, everything else: log.
|
|
elseif response.status_code ~= 401 and response.status_code ~= 403 then
|
|
txn:Warning("Invalid status code in auth-request backend '" .. be .. "': " .. response.status_code)
|
|
end
|
|
end
|