| -- 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 |
| |
| -- Downloaded from: https://github.com/TimWolla/haproxy-auth-request |
| |
| 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 |