Skip to content

Instantly share code, notes, and snippets.

@bobvanderlinden
Created October 30, 2019 16:40
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bobvanderlinden/43faa3c91895e6704974d813789d67d8 to your computer and use it in GitHub Desktop.
Save bobvanderlinden/43faa3c91895e6704974d813789d67d8 to your computer and use it in GitHub Desktop.
HTTP path pattern matching for HAProxy using LUA
local function matches(input, pattern)
local index = 1
return function()
local match = input:match(pattern, index)
if match then
index = index + match:len() + 1
return match
else
return nil
end
end
end
local function to_array(iterator)
local result = {}
for v in iterator do
table.insert(result, v)
end
return result
end
local function variables(path)
return matches(path, "{([^}]+)}")
end
local function path_segments(path)
return matches(path, "/([^/]+)")
end
local function parse_query(query)
query = "?" .. query
local index = 1
return function()
local key, value = query:match("[?&]([a-zA-Z0-9_]+)=([^&?]*)", index)
if key then
index = index + key:len() + value:len() + 1
return key, value
else
return nil
end
end
end
local function split_url(url)
local path, _, query = url:match("^(.*)(?(.*))\\?$")
if path then
return path, query
else
return url, nil
end
end
local function parse_url_pattern(url_pattern)
local path, query = split_url(url_pattern)
-- print("url_pattern", url_pattern, "path", path, "query", query)
local parameters_lookup = {}
local parameters = {}
local segments = {}
for segment in path_segments(path) do
local variable_name = segment:match("^{([a-zA-Z0-9_]+)}$")
if variable_name then
table.insert(segments, "*")
local parameter = {
name = variable_name,
variable_name = variable_name,
["in"] = "path"
}
parameters_lookup[variable_name] = parameter
table.insert(parameters, parameter)
else
table.insert(segments, segment)
end
end
if query then
for name, value in parse_query(query) do
local variable_name = value:match("^{([a-zA-Z0-9_]+)}$")
local parameter = {
name = name,
variable_name = variable_name,
["in"] = "query"
}
parameters_lookup[variable_name] = parameter
table.insert(parameters, parameter)
end
end
return {
url = url_pattern,
path = path,
query = query,
segments = segments,
parameters_lookup = parameters_lookup,
parameters = parameters
}
end
local function create_node(root, segments)
local node = root
for _, segment in ipairs(segments) do
if node[segment] then
node = node[segment]
else
node[segment] = {}
node = node[segment]
end
end
return node
end
local function join(array, separator)
return table.concat(array, separator)
end
local function dump(node, prefix)
if type(node) == "string" then
print(prefix, node)
return
end
for key, value in pairs(node) do
dump(value, (prefix or '') .. '/' .. key)
end
end
local function match_operation(root, method, path)
local node = root[method]
local variable_values = {}
for segment in path_segments(path) do
if node[segment] then
node = node[segment]
elseif node['*'] then
node = node['*']
table.insert(variable_values, segment)
else
return nil
end
end
local operation = node['_']
if not operation then
return nil
end
return {
variable_values = variable_values,
operation = node['_']
}
end
local function map_source_url(root, method, url)
local path, query = split_url(url)
local match = match_operation(root, method, path)
if not match then
return nil
end
local operation = match.operation
local variable_values = {}
-- Fetch variable values from path parameters
local path_values = match.variable_values
for index, path_value in pairs(path_values) do
local variable = operation.variables_by_path_index[index]
variable_values[variable.name] = { path_value }
end
if query then
-- Fetch variable values from query parameters
for key, value in parse_query(query) do
local values = variable_values[key]
if values then
table.insert(values, value)
else
variable_values[key] = { value }
end
end
end
-- Add path values for target url
local target_path = operation.target.path:gsub("{([a-zA-Z0-9_]+)}", function(variable_name)
return variable_values[variable_name][1]
end)
-- Add query values for target url
local target_query_segments = {}
for _, parameter in ipairs(operation.target.parameters) do
local variable_name = parameter.variable_name
if parameter["in"] == "query" then
local values = variable_values[variable_name]
if values then
for _, value in ipairs(values) do
table.insert(target_query_segments, parameter.name .. "=" .. value)
end
end
end
end
local target_query = join(target_query_segments, "&")
local target_url
if target_query == "" then
target_url = target_path
else
target_url = target_path .. "?" .. target_query
end
return {
operation = operation,
target_url = target_url
}
end
local root = {
get = {},
put = {},
delete = {},
post = {}
}
for line in io.lines("urls.map") do
operation_id, method, source_pattern, target_pattern, target_deployment = line:match("^(.*) (.*) (.*) (.*) (.*)$")
local source = parse_url_pattern(source_pattern)
local target = parse_url_pattern(target_pattern)
local variables = {}
local variables_by_path_index = {}
local variables_by_query_name = {}
for _, source_parameter in ipairs(source.parameters) do
local variable_name = source_parameter.variable_name
local target_parameter = target.parameters_lookup[variable_name]
local variable = {
name = variable_name,
source_parameter = source_parameter,
target_parameter = target_parameter
}
variables[variable_name] = variable
if source_parameter["in"] == "query" then
variables_by_query_name[source_parameter.name] = variable
elseif source_parameter["in"] == "path" then
table.insert(variables_by_path_index, variable)
end
end
local operation = {
operation_id = operation_id,
method = method,
source = source,
target = target,
target_deployment = target_deployment,
variables = variables,
variables_by_path_index = variables_by_path_index,
variables_by_query_name = variables_by_query_name
}
local node = create_node(root[method], source.segments)
node['_'] = operation
end
local function map_http_call(txn)
local url = txn.f:url()
local method = txn.f:method()
method = method:lower()
print(method, url)
local mapping = map_source_url(root, method, url)
if mapping then
txn:set_var("txn.operation_id", mapping.operation.operation_id)
txn:set_var("txn.target_deployment", mapping.operation.target_deployment)
txn.http:req_set_uri(mapping.target_url)
end
end
global
log /dev/stdout local0 debug
debug
lua-load experiment.lua
defaults
mode http
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
backend no-match
http-request deny deny_status 400
backend backend_a
server backend_a 127.0.0.1:8001
backend backend_b
server backend_b 127.0.0.1:1234
frontend http
bind 0.0.0.0:8080
http-request lua.map_http_call
# Headers sent to the target deployment.
http-request set-header x-cupido-operation-id %[var(txn.operation_id)]
http-request set-header x-cupido-target-deployment %[var(txn.target_deployment)]
# Headers sent back to the client.
http-response set-header x-cupido-operation-id %[var(txn.operation_id)]
http-response set-header x-cupido-target-deployment %[var(txn.target_deployment)]
use_backend %[var(txn.target_deployment)]
default_backend no-match
operation_a get /user/{user_id}/logs?date={date} /user?user_id={user_id}&find_date={date} backend_a
operation_b get /user/{user_id}/something?another={another} /api/user{user_id}/something?another={another} backend_b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment