1
0

init commit

This commit is contained in:
2022-11-01 23:31:14 +08:00
commit 57c2c6a22e
26 changed files with 2208 additions and 0 deletions

7
plugins/core.lua Normal file
View File

@@ -0,0 +1,7 @@
return {
crypto = require("core.crypto"),
json = require("core.json"),
table = require("core.table"),
request = require("core.request"),
}

39
plugins/crypto.lua Normal file
View File

@@ -0,0 +1,39 @@
local ffi = require('ffi')
local crypto = ffi.load("crypto.so.1.1", true)
local ffi_string = ffi.string
ffi.cdef[[
char *MD5(const char *d, size_t n, char *md);
int EVP_DecodeBlock(unsigned char *t, const unsigned char *f, int n);
int EVP_EncodeBlock(unsigned char *t, const unsigned char *f, int n);
]]
local _M = { version = 0.2 }
function string.tohex(str)
return (str:gsub('.', function (c)
return string.format('%02X', string.byte(c))
end))
end
function _M.md5(data)
local buf = ffi.new("char[16]")
crypto.MD5(data, #data, buf)
return ffi_string(buf):tohex()
end
function _M.decode_base64(data)
local len = #data - #data/4
local buf = ffi.new("unsigned char["..len.."]")
len = crypto.EVP_DecodeBlock(buf, data, #data)
return ffi_string(buf, len - 1)
end
function _M.encode_base64(data)
local len = #data + #data/4
local buf = ffi.new("unsigned char["..len.."]")
len = crypto.EVP_EncodeBlock(buf, data, #data)
return ffi_string(buf, len - 1)
end
return _M

46
plugins/ctx.lua Normal file
View File

@@ -0,0 +1,46 @@
local _M = { version = 0.2 }
local function get_client_ip(stream)
local ip = stream:downstreamLocalAddress()
if ip then
return ip
end
ip = stream:downstreamDirectRemoteAddress()
if ip then
return ip
end
end
function _M.set_vars_meta(handle)
local stream = handle:streamInfo()
local meta = stream:dynamicMetadata()
local var = {}
var._cache = meta:get("envoy.filters.http.lua") or {}
var.remote_addr = get_client_ip(stream)
setmetatable(var, {
__index = function(self, key)
local cached = self._cache[key]
if cached ~= nil then
return cached
end
return nil
end,
__newindex = function(self, key, val)
meta:set("envoy.filters.http.lua", key, val)
self._cache[key] = val
end,
__pairs = function (self)
return next, self._cache, nil
end,
})
return var
end
return _M

15
plugins/entry.lua Normal file
View File

@@ -0,0 +1,15 @@
local plugin = require("core.plugin")
function envoy_on_request(request_handle)
local conf = request_handle:metadata():get("plugins")
if conf then
plugin.run(request_handle, "request", conf)
end
end
function envoy_on_response(response_handle)
local conf = response_handle:metadata():get("plugins")
if conf then
plugin.run(response_handle, "response", conf)
end
end

110
plugins/envoy.yaml Normal file
View File

@@ -0,0 +1,110 @@
# docker run --rm --network host -v $(pwd):/etc/envoy -w /etc/envoy -it envoyproxy/envoy-distroless:v1.24.0
# curl -u 'admin:admin' -H 'Origin: http://lenovo.com' -i http://vcap.me:10000/ip
static_resources:
listeners:
- address:
socket_address:
address: 0.0.0.0
port_value: 10000
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
codec_type: AUTO
stat_prefix: ingress_http
strip_any_host_port: true
route_config:
name: local_route
virtual_hosts:
- name: vcap_me
domains:
- vcap.me
routes:
- match:
path: /ip
route:
auto_host_rewrite: true
cluster: httpbin
metadata:
filter_metadata:
envoy.filters.http.lua:
plugins:
- name: user-code
conf:
access: |
ctx.var.requst = {
path = ctx.headers:get(":path")
}
- name: basic-auth
conf:
username: admin
password: admin
- name: redirect
conf:
ret_code: 301
uri: /headers
headers:
x-earth-token: xxx
x-earth-project: yyy
- match:
path: /body
direct_response:
status: 200
body:
inline_string: "Body\n"
metadata:
filter_metadata:
envoy.filters.http.lua:
plugins:
- name: user-code
conf:
access: |
ctx.var.path = ctx.headers:get(":path")
ctx.var.method = ctx.headers:get(":method")
ctx.log.info("hit access")
body_filter: |
for key, value in pairs(ctx.var) do
ctx.log.info("var: ", key, " = ", value)
end
ctx.log.info("hit body_filter")
- match:
path: /auth
direct_response:
status: 200
body:
inline_string: "Autherd\n"
metadata:
filter_metadata:
envoy.filters.http.lua:
plugins:
- name: basic-auth
conf:
htpasswd: | # htpasswd -bnBC 10 admin admin
admin:$2y$10$7y/gRzOG6zhB5WOnGp8xw.wMF9c4Fw6ZkPwaALHlMNFG5IZy1W3Um
http_filters:
- name: envoy.filters.http.lua
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
default_source_code:
filename: /etc/envoy/entry.lua
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
access_log:
- name: envoy.access_loggers.stdout
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
clusters:
- name: httpbin
type: LOGICAL_DNS
load_assignment:
cluster_name: httpbin
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: httpbin.org
port_value: 80

388
plugins/json.lua Normal file
View File

@@ -0,0 +1,388 @@
--
-- json.lua
--
-- Copyright (c) 2020 rxi
--
-- 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.
--
local json = { _version = "0.1.2" }
-------------------------------------------------------------------------------
-- Encode
-------------------------------------------------------------------------------
local encode
local escape_char_map = {
[ "\\" ] = "\\",
[ "\"" ] = "\"",
[ "\b" ] = "b",
[ "\f" ] = "f",
[ "\n" ] = "n",
[ "\r" ] = "r",
[ "\t" ] = "t",
}
local escape_char_map_inv = { [ "/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if rawget(val, 1) ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
error("invalid table: sparse array")
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
end
-------------------------------------------------------------------------------
-- Decode
-------------------------------------------------------------------------------
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
local line_count = 1
local col_count = 1
for i = 1, idx - 1 do
col_count = col_count + 1
if str:sub(i, i) == "\n" then
line_count = line_count + 1
col_count = 1
end
end
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(1, 4), 16 )
local n2 = tonumber( s:sub(7, 10), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local res = ""
local j = i + 1
local k = j
while j <= #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
elseif x == 92 then -- `\`: Escape
res = res .. str:sub(k, j - 1)
j = j + 1
local c = str:sub(j, j)
if c == "u" then
local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
or str:match("^%x%x%x%x", j + 1)
or decode_error(str, j - 1, "invalid unicode escape in string")
res = res .. parse_unicode_escape(hex)
j = j + #hex
else
if not escape_chars[c] then
decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
end
res = res .. escape_char_map_inv[c]
end
k = j + 1
elseif x == 34 then -- `"`: End of string
res = res .. str:sub(k, j - 1)
return res, j + 1
end
j = j + 1
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
local res, idx = parse(str, next_char(str, 1, space_chars, true))
idx = next_char(str, idx, space_chars, true)
if idx <= #str then
decode_error(str, idx, "trailing garbage")
end
return res
end
return json

41
plugins/log.lua Normal file
View File

@@ -0,0 +1,41 @@
local _M = { }
local log_levels = {
crit = "logCritical",
error = "logErr",
warn = "logWarn",
notice = "logTrace",
info = "logInfo",
debug = "logDebug",
}
local _tostring = tostring
local tostring = function(...)
local t = {}
for i = 1, select('#', ...) do
local x = select(i, ...)
if type(x) == "number" then
x = round(x, .01)
end
t[#t + 1] = _tostring(x)
end
return table.concat(t, " ")
end
function _M.new(handle)
local o = {}
setmetatable(o, {__index = function(self, cmd)
local t = getmetatable(handle)
local method = rawget(t, log_levels[cmd])
if not method then
return do_nothing
end
return function(...)
method(handle, tostring(...))
end
end})
return o
end
return _M

62
plugins/plugin.lua Normal file
View File

@@ -0,0 +1,62 @@
local log = require("core.log")
local ctx = require("core.ctx")
local json = require("core.json")
local _M = {version = 0.2}
local phases = {
request = {
'access',
'rewrite'
},
response = {
'header_filter',
'body_filter'
}
}
function _M.run(handle, phase, plugins)
if not plugins or #plugins == 0 then
return ctx
end
local resp_header = {}
local context = {
headers = handle:headers(),
body = handle:body(),
log = log.new(handle),
var = ctx.set_vars_meta(handle),
}
function context.set_resp_header(key, value)
resp_header[key] = value
end
for _, plugin in ipairs(plugins) do
local ok, plugin_object = pcall(require, "plugins." .. plugin.name)
if ok then
local earth_phases = phases[phase]
for _, phase_name in ipairs(earth_phases) do
local phase_func = plugin_object[phase_name]
if type(phase_func) == "function" then
handle:logTrace("phase_name: " .. plugin.name .. "." .. phase_name)
local status, body = phase_func(plugin.conf, context)
if status then
resp_header[":status"] = status
if type(body) == "table" then
body = json.encode(body)
end
return handle:respond(resp_header, body)
end
end
end
else
handle:logWarn("failed to load plugin ["..plugin.name.."] err: "..plugin_object)
end
end
return ctx
end
return _M

View File

@@ -0,0 +1,64 @@
local core = require("core")
local plugin_name = "basic-auth"
local _M = {
version = 0.1,
priority = 2520,
type = 'auth',
name = plugin_name,
}
local function extract_auth_header(auth)
local obj = { username = "", password = "" }
local userpass = auth:match("Basic%s+(.*)")
if not userpass then
return nil, "Invalid authorization header format"
end
local decoded = core.crypto.decode_base64(userpass)
if not decoded then
return nil, "Failed to decode authentication header: " .. m[1]
end
user, pass = decoded:match("([^:]*):(.*)")
obj.username = user:gsub("%s+", "")
obj.password = pass:gsub("%s+", "")
return obj, nil
end
function _M.rewrite(conf, ctx)
ctx.log.info("plugin access phase, conf: ", core.json.encode(conf))
-- 1. extract authorization from header
local auth_header = core.request.header(ctx, "Authorization")
if not auth_header then
ctx.set_resp_header("WWW-Authenticate", "Basic realm='.'")
return 401, { message = "Missing authorization in request" }
end
local user, err = extract_auth_header(auth_header)
if err then
ctx.log.warn("extract auth header: ", err)
return 401, { message = "Invalid authorization in request" }
end
ctx.log.info("plugin access phase, authorization: ", user.username, ":", user.password)
-- 2. get user info from cache
-- 4. check the password is correct
if conf.password ~= user.password then
ctx.log.info("check: ["..type(conf.password).."], ["..type(user.password).."]")
ctx.log.info("check: ["..#conf.password.."], ["..#user.password.."\0".."]")
return 401, { message = "Invalid user authorization" }
end
-- 5. hide `Authorization` request header if `hide_credentials` is `true`
if conf.hide_credentials then
ctx.headers:remove("Authorization")
end
ctx.log.info("hit basic-auth access")
end
return _M

View File

@@ -0,0 +1,84 @@
local core = require("core")
local ldap = require("resty.ldap")
local plugin_name = "ldap-auth"
local _M = {
version = 0.1,
priority = 2540,
type = 'auth',
name = plugin_name,
}
local function extract_auth_header(auth)
local obj = { username = "", password = "" }
local userpass = auth:match("Basic%s+(.*)")
if not userpass then
return nil, "Invalid authorization header format"
end
local decoded = core.crypto.decode_base64(userpass)
if not decoded then
return nil, "Failed to decode authentication header: " .. m[1]
end
user, pass = decoded:match("([^:]*):(.*)")
obj.username = user:gsub("%s+", "")
obj.password = pass:gsub("%s+", "")
return obj, nil
end
function _M.rewrite(conf, ctx)
ctx.log.info("plugin rewrite phase, conf: ", core.json.encode(conf))
-- 1. extract authorization from header
local auth_header = core.request.header(ctx, "Authorization")
if not auth_header then
ctx.set_resp_header("WWW-Authenticate", "Basic realm='.'")
return 401, { message = "Missing authorization in request" }
end
local user, err = extract_auth_header(auth_header)
if err then
ctx.log.warn(err)
return 401, { message = "Invalid authorization in request" }
end
-- 2. try authenticate the user against the ldap server
local ldap_host, ldap_port = core.utils.parse_addr(conf.ldap_uri)
local userdn = conf.uid .. "=" .. user.username .. "," .. conf.base_dn
local ldapconf = {
timeout = 10000,
start_tls = false,
ldap_host = ldap_host,
ldap_port = ldap_port or 389,
ldaps = conf.use_tls,
tls_verify = conf.tls_verify,
base_dn = conf.base_dn,
attribute = conf.uid,
keepalive = 60000,
}
local res, err = ldap.ldap_authenticate(user.username, user.password, ldapconf)
if not res then
ctx.log.warn("ldap-auth failed: ", err)
return 401, { message = "Invalid user authorization" }
end
-- 3. Retrieve consumer for authorization plugin
local consumer_conf = consumer_mod.plugin(plugin_name)
if not consumer_conf then
return 401, { message = "Missing related consumer" }
end
local consumers = lrucache("consumers_key", consumer_conf.conf_version,
create_consumer_cache, consumer_conf)
local consumer = consumers[userdn]
if not consumer then
return 401, {message = "Invalid user authorization"}
end
consumer_mod.attach_consumer(ctx, consumer, consumer_conf)
ctx.log.info("hit basic-auth access")
end
return _M

View File

@@ -0,0 +1,26 @@
local core = require("core")
local plugin_name = "redirect"
local _M = {
version = 0.1,
priority = 900,
name = plugin_name
}
function _M.rewrite(conf, ctx)
local ret_code = conf.ret_code
local uri = conf.uri
if conf.http_to_https and ctx.var.scheme == "http" then
uri = "https://$host$request_uri"
ret_code = 301
end
if uri and ret_code then
ctx.set_resp_header("Location", uri)
return ret_code
end
end
return _M

View File

@@ -0,0 +1,29 @@
local plugin_name = "user-code"
local _M = {
version = 0.1,
priority = 2520,
name = plugin_name
}
local function eval(equation, variables)
if(type(equation) == "string") then
local eval = load(equation);
if(type(eval) == "function") then
setfenv(eval, variables or {});
return eval();
end
end
end
setmetatable(_M, {__index = function(self, cmd)
return function(conf, ctx)
local equation = rawget(conf, cmd)
return eval(equation, {
pairs = pairs,
ctx = ctx,
})
end
end})
return _M

30
plugins/request.lua Normal file
View File

@@ -0,0 +1,30 @@
local encode_json = require("core.json").encode
local concat_tab = table.concat
local _M = { version = 0.1 }
function _M.header(ctx, name)
return ctx.headers:get(name)
end
function _M.set_header(ctx, name, value)
return ctx.headers:replace(name, value)
end
function _M.get_ip(ctx)
return ctx.var.remote_addr or ctx.headers:get("x-forwarded-for")
end
function _M.getbody(max_size, ctx)
return ctx.body:getBytes(0, max_size)
end
function _M.get_scheme(ctx)
return ctx.headers:get(":scheme")
end
function _M.get_host(ctx)
return ctx.headers:get(":authority")
end
return _M

68
plugins/string.lua Normal file
View File

@@ -0,0 +1,68 @@
--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
local error = error
local type = type
local str_find = string.find
local ffi = require("ffi")
local C = ffi.C
local ffi_cast = ffi.cast
ffi.cdef[[
int memcmp(const void *s1, const void *s2, size_t n);
]]
local _M = {
version = 0.1,
}
setmetatable(_M, {__index = string})
-- find a needle from a haystack in the plain text way
function _M.find(haystack, needle, from)
return str_find(haystack, needle, from or 1, true)
end
function _M.has_prefix(s, prefix)
if type(s) ~= "string" or type(prefix) ~= "string" then
error("unexpected type: s:" .. type(s) .. ", prefix:" .. type(prefix))
end
if #s < #prefix then
return false
end
local rc = C.memcmp(s, prefix, #prefix)
return rc == 0
end
function _M.has_suffix(s, suffix)
if type(s) ~= "string" or type(suffix) ~= "string" then
error("unexpected type: s:" .. type(s) .. ", suffix:" .. type(suffix))
end
if #s < #suffix then
return false
end
local rc = C.memcmp(ffi_cast("char *", s) + #s - #suffix, suffix, #suffix)
return rc == 0
end
return _M

162
plugins/table.lua Normal file
View File

@@ -0,0 +1,162 @@
--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
local newproxy = newproxy
local getmetatable = getmetatable
local setmetatable = setmetatable
local select = select
local new_tab = require("table.new")
local pairs = pairs
local type = type
local string = string
local _M = {
version = 0.2,
new = new_tab,
clear = require("table.clear"),
insert = table.insert,
concat = table.concat,
sort = table.sort,
}
setmetatable(_M, {__index = table})
local nkeys
do
local ok, table_nkeys = pcall(require, 'table.nkeys')
if ok then
nkeys = table_nkeys
else
nkeys = function(t)
local count = 0
for _, _ in pairs(t) do
count = count + 1
end
return count
end
end
end
_M.nkeys = nkeys
function _M.insert_tail(tab, ...)
local idx = #tab
for i = 1, select('#', ...) do
idx = idx + 1
tab[idx] = select(i, ...)
end
return idx
end
function _M.set(tab, ...)
for i = 1, select('#', ...) do
tab[i] = select(i, ...)
end
end
-- only work under lua51 or luajit
function _M.setmt__gc(t, mt)
local prox = newproxy(true)
getmetatable(prox).__gc = function() mt.__gc(t) end
t[prox] = true
return setmetatable(t, mt)
end
local function deepcopy(orig)
local orig_type = type(orig)
if orig_type ~= 'table' then
return orig
end
-- If the array-like table contains nil in the middle,
-- the len might be smaller than the expected.
-- But it doesn't affect the correctness.
local len = #orig
local copy = new_tab(len, nkeys(orig) - len)
for orig_key, orig_value in pairs(orig) do
copy[orig_key] = deepcopy(orig_value)
end
return copy
end
_M.deepcopy = deepcopy
local ngx_null = nil
local function merge(origin, extend)
for k,v in pairs(extend) do
if type(v) == "table" then
if type(origin[k] or false) == "table" then
if _M.nkeys(origin[k]) ~= #origin[k] then
merge(origin[k] or {}, extend[k] or {})
else
origin[k] = v
end
else
origin[k] = v
end
elseif v == ngx_null then
origin[k] = nil
else
origin[k] = v
end
end
return origin
end
_M.merge = merge
local function patch(node_value, sub_path, conf)
local sub_value = node_value
local sub_paths = string.split(sub_path, "/")
for i = 1, #sub_paths - 1 do
local sub_name = sub_paths[i]
if sub_value[sub_name] == nil then
sub_value[sub_name] = {}
end
sub_value = sub_value[sub_name]
if type(sub_value) ~= "table" then
return 400, "invalid sub-path: /"
.. _M.concat(sub_paths, 1, i)
end
end
if type(sub_value) ~= "table" then
return 400, "invalid sub-path: /" .. sub_path
end
local sub_name = sub_paths[#sub_paths]
if sub_name and sub_name ~= "" then
sub_value[sub_name] = conf
else
node_value = conf
end
return nil, nil, node_value
end
_M.patch = patch
return _M