Created
October 30, 2019 16:40
-
-
Save bobvanderlinden/43faa3c91895e6704974d813789d67d8 to your computer and use it in GitHub Desktop.
HTTP path pattern matching for HAProxy using LUA
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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