commit 57c2c6a22e3a41250dd03b401d97c41ee9017bd2 Author: dragonflylee Date: Tue Nov 1 23:31:14 2022 +0800 init commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d88ed8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.o +*.dylib +/bin/* +/testbin/* +/luajit/* + +# User-specific files +*.suo +*.user +*.sln.docstates +/.vscode + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +x64/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# NuGet Packages Directory +packages/* + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +envoy-apisix diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6e0355b --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +TARGET = envoy-apisix +LUA_VERSION = 2.1.0-beta3 + +SRCS = $(wildcard envoy/*.cpp) +OBJS = $(SRCS:.cpp=.o) + +INCLUDES = -I./luajit/include/luajit-2.1 +CXXFLAGS = -fPIC -O2 -Wall -std=c++11 $(INCLUDES) +LDFLAGS = -L./luajit/lib -lluajit-5.1 -ldl + +.PHONY: all +all: run + +$(TARGET): $(OBJS) + $(CXX) -o $@ $^ $(LDFLAGS) + +luajit: + $(RM) -r /tmp/LuaJIT-$(LUA_VERSION) + curl -sL https://luajit.org/download/LuaJIT-$(LUA_VERSION).tar.gz | tar zxf - -C /tmp + make -C /tmp/LuaJIT-$(LUA_VERSION) install PREFIX=$(shell pwd)/luajit XCFLAGS=-DLUAJIT_ENABLE_LUA52COMPAT + +.PHONY: run +run: ${TARGET} + @./$(TARGET) ./plugins/entry.lua + +.PHONY: envoy +envoy: + docker run -it --rm --net host -v $(shell pwd):/etc/envoy -e "LUA_PATH=/etc/envoy/?.lua" envoyproxy/envoy-distroless:v1.24.0 + +.PHONY: clean +clean: + ${RM} ${TARGET} ${OBJS} diff --git a/envoy.sln b/envoy.sln new file mode 100644 index 0000000..20c4d2f --- /dev/null +++ b/envoy.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Express 2013 for Windows Desktop +VisualStudioVersion = 12.0.40629.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "envoy", "envoy\envoy.vcxproj", "{E152FBD5-2A29-400E-8E18-9C515986217C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Release|Win32 = Release|Win32 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E152FBD5-2A29-400E-8E18-9C515986217C}.Debug|Win32.ActiveCfg = Debug|Win32 + {E152FBD5-2A29-400E-8E18-9C515986217C}.Debug|Win32.Build.0 = Debug|Win32 + {E152FBD5-2A29-400E-8E18-9C515986217C}.Release|Win32.ActiveCfg = Release|Win32 + {E152FBD5-2A29-400E-8E18-9C515986217C}.Release|Win32.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/envoy.yaml b/envoy.yaml new file mode 100644 index 0000000..b16a9f8 --- /dev/null +++ b/envoy.yaml @@ -0,0 +1,126 @@ +# 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 + generate_request_id: false + 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: + body_filter: | + for key, value in pairs(ctx.headers) do + log.info("user-code: ", key, " = ", value) + end + - 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: "Get Body\n" + metadata: + filter_metadata: + envoy.filters.http.lua: + plugins: + - name: user-code + conf: + access: | + ctx.var.path = ctx.headers:get(":path") + 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 + - match: + prefix: / + direct_response: + status: 200 + body: + inline_string: "Lucky\n" + metadata: + filter_metadata: + envoy.filters.http.lua: + plugins: + - name: user-code + conf: + body_filter: | + for key, value in pairs(ctx.headers) do + ctx.log.info("header: ", key, " = ", value) + end + 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/core/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 diff --git a/envoy/envoy.cpp b/envoy/envoy.cpp new file mode 100644 index 0000000..94f7e19 --- /dev/null +++ b/envoy/envoy.cpp @@ -0,0 +1,204 @@ + +#include "envoy.h" +#include +#include + +namespace lua { + +/** + * A wrapper for a buffer. + */ +int BufferWrapper::luaLength(lua_State* state) { + lua_pushnumber(state, 0); + return 1; +} + +int BufferWrapper::luaGetBytes(lua_State* state) { + lua_pushlstring(state, "buffer", 6); + return 1; +} + +int BufferWrapper::luaSetBytes(lua_State* state) { + lua_pushnumber(state, 0); + return 1; +} + +/** + * Lua wrapper for a header map. Methods that will modify the map will call a check function + * to see if modification is allowed. + */ +int HeaderMapWrapper::luaAdd(lua_State* state) { + UNUSED(state); + return 0; +} + +int HeaderMapWrapper::luaGet(lua_State* state) { + UNUSED(state); + return 0; +} + +int HeaderMapWrapper::luaPairs(lua_State* state) { + if (iterator_.get() != nullptr) { + luaL_error(state, "cannot create a second iterator before completing the first"); + } + iterator_.reset(HeaderMapIterator::create(state), true); + lua_pushcclosure(state, HeaderMapIterator::static_luaPairsIterator, 1); + return 1; +} + +int HeaderMapIterator::luaPairsIterator(lua_State* state) { + UNUSED(state); + return 0; +} + + +/** + * Lua wrapper for a metadata map. + */ +int MetadataMapWrapper::luaGet(lua_State* state) { + const char* key = luaL_checkstring(state, 2); + UNUSED(key); + return 0; +} + +int MetadataMapWrapper::luaPairs(lua_State* state) { + if (iterator_.get() != nullptr) { + luaL_error(state, "cannot create a second iterator before completing the first"); + } + iterator_.reset(MetadataMapIterator::create(state), true); + lua_pushcclosure(state, MetadataMapIterator::static_luaPairsIterator, 1); + return 1; +} + +int MetadataMapIterator::luaPairsIterator(lua_State* state) { + UNUSED(state); + return 0; +} + +/** + * Lua wrapper for a stream info. + */ +int StreamInfoWrapper::luaProtocol(lua_State* state) { + const std::string protocol = "HTTP/1.1"; + lua_pushlstring(state, protocol.data(), protocol.size()); + return 1; +} + +int StreamInfoWrapper::luaDynamicMetadata(lua_State* state) { + UNUSED(state); + return 0; +} + +int StreamInfoWrapper::luaDownstreamSslConnection(lua_State* state) { + lua_pushnil(state); + return 1; +} + +int StreamInfoWrapper::luaDownstreamLocalAddress(lua_State* state) { + const std::string local_address = "127.0.0.1:1234"; + lua_pushlstring(state, local_address.data(), local_address.size()); + return 1; +} + +int StreamInfoWrapper::luaDownstreamDirectRemoteAddress(lua_State* state) { + const std::string direct_remote_address = "172.17.0.3:80"; + lua_pushlstring(state, direct_remote_address.data(), direct_remote_address.size()); + return 1; +} + +int StreamInfoWrapper::luaRequestedServerName(lua_State* state) { + const std::string server_name = "envoy"; + lua_pushlstring(state, server_name.data(), server_name.size()); + return 1; +} + +/** + * A wrapper for a currently running request/response. This is the primary handle passed to Lua. + * The script interacts with Envoy entirely through this handle. + */ +int StreamHandleWrapper::luaHeaders(lua_State* state) { + if (headers_wrapper_.get() != nullptr) { + headers_wrapper_.pushStack(); + } else { + headers_wrapper_.reset(HeaderMapWrapper::create(state), true); + } + return 1; +} + +int StreamHandleWrapper::luaBody(lua_State* state) { + UNUSED(state); + return 0; +} + +int StreamHandleWrapper::luaBodyChunks(lua_State* state) { + lua_pushcclosure(state, static_luaBodyIterator, 1); + return 1; +} + +int StreamHandleWrapper::luaBodyIterator(lua_State* state) { + LuaDeathRef wrapper; + wrapper.reset(BufferWrapper::create(state), true); + return 1; +} + +int StreamHandleWrapper::luaMetadata(lua_State* state) { + if (metadata_wrapper_.get() != nullptr) { + metadata_wrapper_.pushStack(); + } else { + metadata_wrapper_.reset(MetadataMapWrapper::create(state), true); + } + return 1; +} + +int StreamHandleWrapper::luaStreamInfo(lua_State* state) { + if (stream_info_wrapper_.get() != nullptr) { + stream_info_wrapper_.pushStack(); + } else { + stream_info_wrapper_.reset(StreamInfoWrapper::create(state), true); + } + return 1; +} + +int luaLog(lua_State* state, const char* prefix) { + size_t input_size = 0; + const char* input = luaL_checklstring(state, 2, &input_size); + std::string message(input, input_size); + std::cout << prefix << ": " << message << std::endl; + return 0; +} + +int StreamHandleWrapper::luaLogTrace(lua_State* state) { + return luaLog(state, "trace"); +} + +int StreamHandleWrapper::luaLogDebug(lua_State* state) { + return luaLog(state, "debug"); +} + +int StreamHandleWrapper::luaLogInfo(lua_State* state) { + return luaLog(state, "info"); +} + +int StreamHandleWrapper::luaLogWarn(lua_State* state) { + return luaLog(state, "warn"); +} + +int StreamHandleWrapper::luaLogErr(lua_State* state) { + return luaLog(state, "error"); +} + +int StreamHandleWrapper::luaLogCritical(lua_State* state) { + return luaLog(state, "crit"); +} + +int StreamHandleWrapper::luaHttpCall(lua_State* state) { + UNUSED(state); + return 0; +} + +int StreamHandleWrapper::luaRespond(lua_State* state) { + UNUSED(state); + return 0; +} + +} \ No newline at end of file diff --git a/envoy/envoy.h b/envoy/envoy.h new file mode 100644 index 0000000..bd76034 --- /dev/null +++ b/envoy/envoy.h @@ -0,0 +1,130 @@ +#pragma once + +#include "lua.h" + +namespace lua { + +class BufferWrapper : public BaseLuaObject { +public: + static ExportedFunctions exportedFunctions() { + return {{"length", static_luaLength}, + {"getBytes", static_luaGetBytes}, + {"setBytes", static_luaSetBytes}}; + } +private: + DECLARE_LUA_FUNCTION(BufferWrapper, luaLength); + DECLARE_LUA_FUNCTION(BufferWrapper, luaGetBytes); + DECLARE_LUA_FUNCTION(BufferWrapper, luaSetBytes); +}; + +class HeaderMapIterator : public BaseLuaObject { +public: + static ExportedFunctions exportedFunctions() { return {}; } + + DECLARE_LUA_CLOSURE(HeaderMapIterator, luaPairsIterator); +}; + +class HeaderMapWrapper : public BaseLuaObject { +public: + static ExportedFunctions exportedFunctions() { + return { + { "add", static_luaAdd }, + { "get", static_luaGet }, + { "__pairs", static_luaPairs }, + }; + } + +private: + DECLARE_LUA_FUNCTION(HeaderMapWrapper, luaAdd); + DECLARE_LUA_FUNCTION(HeaderMapWrapper, luaGet); + DECLARE_LUA_FUNCTION(HeaderMapWrapper, luaPairs); + + LuaDeathRef iterator_; +}; + +class MetadataMapIterator : public BaseLuaObject { +public: + static ExportedFunctions exportedFunctions() { return {}; } + + DECLARE_LUA_CLOSURE(MetadataMapIterator, luaPairsIterator); +}; + +class MetadataMapWrapper : public BaseLuaObject { +public: + static ExportedFunctions exportedFunctions() { + return {{"get", static_luaGet}, {"__pairs", static_luaPairs}}; + } + +private: + DECLARE_LUA_FUNCTION(MetadataMapWrapper, luaGet); + DECLARE_LUA_FUNCTION(MetadataMapWrapper, luaPairs); + + LuaDeathRef iterator_; +}; + +class StreamInfoWrapper : public BaseLuaObject { +public: + static ExportedFunctions exportedFunctions() { + return { + { "protocol", static_luaProtocol }, + { "dynamicMetadata", static_luaDynamicMetadata }, + { "downstreamLocalAddress", static_luaDownstreamLocalAddress }, + { "downstreamDirectRemoteAddress", static_luaDownstreamDirectRemoteAddress }, + { "downstreamSslConnection", static_luaDownstreamSslConnection }, + { "requestedServerName", static_luaRequestedServerName } + }; + } +private: + DECLARE_LUA_FUNCTION(StreamInfoWrapper, luaProtocol); + DECLARE_LUA_FUNCTION(StreamInfoWrapper, luaDynamicMetadata); + DECLARE_LUA_FUNCTION(StreamInfoWrapper, luaDownstreamSslConnection); + DECLARE_LUA_FUNCTION(StreamInfoWrapper, luaDownstreamLocalAddress); + DECLARE_LUA_FUNCTION(StreamInfoWrapper, luaDownstreamDirectRemoteAddress); + DECLARE_LUA_FUNCTION(StreamInfoWrapper, luaRequestedServerName); +}; + +class StreamHandleWrapper : public BaseLuaObject { +public: + static ExportedFunctions exportedFunctions() { + return { + { "headers", static_luaHeaders }, + { "body", static_luaBody }, + { "bodyChunks", static_luaBodyChunks }, + { "metadata", static_luaMetadata }, + { "streamInfo", static_luaStreamInfo }, + { "logTrace", static_luaLogTrace }, + { "logDebug", static_luaLogDebug }, + { "logInfo", static_luaLogInfo }, + { "logWarn", static_luaLogWarn }, + { "logErr", static_luaLogErr }, + { "logCritical", static_luaLogCritical }, + { "httpCall", static_luaHttpCall }, + { "respond", static_luaRespond }, + }; + } + +private: + DECLARE_LUA_FUNCTION(StreamHandleWrapper, luaHeaders); + DECLARE_LUA_FUNCTION(StreamHandleWrapper, luaBody); + DECLARE_LUA_FUNCTION(StreamHandleWrapper, luaBodyChunks); + DECLARE_LUA_CLOSURE(StreamHandleWrapper, luaBodyIterator); + + DECLARE_LUA_FUNCTION(StreamHandleWrapper, luaMetadata); + DECLARE_LUA_FUNCTION(StreamHandleWrapper, luaStreamInfo); + + DECLARE_LUA_FUNCTION(StreamHandleWrapper, luaLogTrace); + DECLARE_LUA_FUNCTION(StreamHandleWrapper, luaLogDebug); + DECLARE_LUA_FUNCTION(StreamHandleWrapper, luaLogInfo); + DECLARE_LUA_FUNCTION(StreamHandleWrapper, luaLogWarn); + DECLARE_LUA_FUNCTION(StreamHandleWrapper, luaLogErr); + DECLARE_LUA_FUNCTION(StreamHandleWrapper, luaLogCritical); + + DECLARE_LUA_FUNCTION(StreamHandleWrapper, luaHttpCall); + DECLARE_LUA_FUNCTION(StreamHandleWrapper, luaRespond); + + LuaDeathRef headers_wrapper_; + LuaDeathRef metadata_wrapper_; + LuaDeathRef stream_info_wrapper_; +}; + +} \ No newline at end of file diff --git a/envoy/envoy.vcxproj b/envoy/envoy.vcxproj new file mode 100644 index 0000000..e4bdc83 --- /dev/null +++ b/envoy/envoy.vcxproj @@ -0,0 +1,105 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + + {E152FBD5-2A29-400E-8E18-9C515986217C} + Win32Proj + envoy + + + + Application + true + v120 + Unicode + + + Application + false + v120 + true + Unicode + + + + + + + + + + + + 3c953278 + + + true + $(SolutionDir)bin\$(Configuration)\$(PlatformTarget)\ + $(SolutionDir)obj\$(Configuration)\$(PlatformTarget)\ + + + false + $(SolutionDir)bin\$(Configuration)\$(PlatformTarget)\ + $(SolutionDir)obj\$(Configuration)\$(PlatformTarget)\ + + + + + + Level3 + Disabled + WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + + + Console + true + + + + + Level3 + + + MaxSpeed + true + true + WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + + + Console + true + true + true + + + + + + + + + + + + + + + + + + + + 这台计算机上缺少此项目引用的 NuGet 程序包。启用“NuGet 程序包还原”可下载这些程序包。有关详细信息,请参阅 http://go.microsoft.com/fwlink/?LinkID=322105。缺少的文件是 {0}。 + + + + \ No newline at end of file diff --git a/envoy/envoy.vcxproj.filters b/envoy/envoy.vcxproj.filters new file mode 100644 index 0000000..09380f5 --- /dev/null +++ b/envoy/envoy.vcxproj.filters @@ -0,0 +1,36 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + 源文件 + + + 源文件 + + + + + 头文件 + + + 头文件 + + + + + + \ No newline at end of file diff --git a/envoy/lua.h b/envoy/lua.h new file mode 100644 index 0000000..c5f00ef --- /dev/null +++ b/envoy/lua.h @@ -0,0 +1,292 @@ +#pragma once + +#include +#include +#include + +#define ASSERT(...) +#define UNUSED(x) (void)(x) + +#ifndef alignof +#define alignof(T) std::alignment_of::value +#endif + +namespace lua { + +/** + * Base macro for declaring a Lua/C function. Any function declared will need to be exported via + * the exportedFunctions() function in BaseLuaObject. See BaseLuaObject below for more + * information. This macro declares a static "thunk" which checks the user data, optionally checks + * for object death (again see BaseLuaObject below for more info), and then invokes a normal + * object method. The actual object method needs to be implemented by the class. + * @param Class supplies the owning class name. + * @param Name supplies the function name. + * @param Index supplies the stack index where "this" (Lua/C userdata) is found. + */ +#define DECLARE_LUA_FUNCTION_EX(Class, Name, Index) \ + static int static_##Name(lua_State* state) { \ + Class* object = lua::alignAndCast( \ + luaL_checkudata(state, Index, typeid(Class).name())); \ + object->checkDead(state); \ + return object->Name(state); \ + } \ + int Name(lua_State* state); + +/** + * Declare a Lua function in which userdata is in stack slot 1. See DECLARE_LUA_FUNCTION_EX() + */ +#define DECLARE_LUA_FUNCTION(Class, Name) DECLARE_LUA_FUNCTION_EX(Class, Name, 1) + +/** + * Declare a Lua function in which userdata is in upvalue slot 1. See DECLARE_LUA_FUNCTION_EX() + */ +#define DECLARE_LUA_CLOSURE(Class, Name) DECLARE_LUA_FUNCTION_EX(Class, Name, lua_upvalueindex(1)) + +/** + * Calculate the maximum space needed to be aligned. + */ +template size_t maximumSpaceNeededToAlign() { + // The allocated memory can be misaligned up to `alignof(T) - 1` bytes. Adding it to the size to + // allocate. + return sizeof(T) + alignof(T) - 1; +} + +template inline T* alignAndCast(void* mem) { + size_t size = maximumSpaceNeededToAlign(); + return static_cast(std::align(alignof(T), sizeof(T), mem, size)); +} + +/** + * Create a new user data and assign its metatable. + */ +template inline T* allocateLuaUserData(lua_State* state) { + void* mem = lua_newuserdata(state, maximumSpaceNeededToAlign()); + luaL_getmetatable(state, typeid(T).name()); + ASSERT(lua_istable(state, -1)); + lua_setmetatable(state, -2); + + return alignAndCast(mem); +} + +/** + * This is the base class for all C++ objects that we expose out to Lua. The goal is to hide as + * much ugliness as possible. In general, to use this, do the following: + * 1) Make your class derive from BaseLuaObject + * 2) Define methods using DECLARE_LUA_FUNCTION* macros + * 3) Export your functions by declaring a static exportedFunctions() method in your class. + * 4) Optionally manage "death" status on your object. (See checkDead() and markDead() below). + * 5) Generally you will want to hold your objects inside a LuaRef or a LuaDeathRef. See below + * for more information on those containers. + * + * It's very important to understand the Lua memory model: Once an object is created, *it is + * owned by Lua*. Lua can GC it at any time. If you want to make sure that does not happen, you + * must hold a ref to it in C++, generally via LuaRef or LuaDeathRef. + */ +template class BaseLuaObject { +public: + using ExportedFunctions = std::vector>; + + virtual ~BaseLuaObject() = default; + + /** + * Create a new object of this type, owned by Lua. This type must have previously been registered + * via the registerType() routine below. + * @param state supplies the owning Lua state. + * @param args supplies the variadic constructor arguments for the object. + * @return a pair containing a pointer to the new object and the state it was created with. (This + * is done for convenience when passing a created object to a LuaRef or a LuaDeathRef. + */ + template + static std::pair create(lua_State* state, ConstructorArgs&&... args) { + // Memory is allocated via Lua and it is raw. We use placement new to run the constructor. + T* mem = allocateLuaUserData(state); + return {new (mem) T(std::forward(args)...), state}; + } + + /** + * Register a type with Lua. + * @param state supplies the state to register with. + */ + static void registerType(lua_State* state) { + std::vector to_register; + + // Fetch all of the functions to be exported to Lua so that we can register them in the + // metatable. + ExportedFunctions functions = T::exportedFunctions(); + for (auto function : functions) { + to_register.push_back({function.first, function.second}); + } + + // Always register a __gc method so that we can run the object's destructor. We do this + // manually because the memory is raw and was allocated by Lua. + to_register.push_back( + {"__gc", [](lua_State* state) { + T* object = alignAndCast(luaL_checkudata(state, 1, typeid(T).name())); + object->~T(); + return 0; + }}); + + // Add the sentinel. + to_register.push_back({nullptr, nullptr}); + + // Register the type by creating a new metatable, setting __index to itself, and then + // performing the register. + luaL_newmetatable(state, typeid(T).name()); + + lua_pushvalue(state, -1); + lua_setfield(state, -2, "__index"); + luaL_register(state, nullptr, to_register.data()); + } + + /** + * This function is called as part of the DECLARE_LUA_FUNCTION* macros. The idea here is that + * we cannot control when Lua destroys things. However, we may expose wrappers to a script that + * should not be used after some event. This allows us to mark objects as dead so that if they + * are used again they will throw a Lua error and not reach our code. + * @param state supplies the calling LuaState. + */ + int checkDead(lua_State* state) { + if (dead_) { + return luaL_error(state, "object used outside of proper scope"); + } + return 0; + } + + /** + * Mark an object as dead so that a checkDead() call will throw an error. See checkDead(). + */ + void markDead() { + dead_ = true; + onMarkDead(); + } + + /** + * Mark an object as live so that a checkDead() call will not throw an error. See checkDead(). + */ + void markLive() { + dead_ = false; + onMarkLive(); + } + +protected: + /** + * Called from markDead() when an object is marked dead. This is effectively a C++ destructor for + * Lua/C objects. Objects can perform inline cleanup or mark other objects as dead if needed. It + * can also be used to protect objects from use if they get assigned to a global variable and + * used across coroutines. + */ + virtual void onMarkDead() {} + + /** + * Called from markLive() when an object is marked live. This is a companion to onMarkDead(). See + * the comments there. + */ + virtual void onMarkLive() {} + +private: + bool dead_{}; +}; + +/** + * This is basically a Lua smart pointer. The idea is that given a Lua object, if we want to + * guarantee that Lua won't destroy it, we need to reference it. This wraps the reference + * functionality. While a LuaRef owns an object it's guaranteed that Lua will not GC it. + * TODO(mattklein123): Add dedicated unit tests. This will require mocking a Lua state. + */ +template class LuaRef { +public: + /** + * Create an empty LuaRef. + */ + LuaRef() { reset(); } + + /** + * Create a LuaRef from an object. + * @param object supplies the object. Generally this is the return value from a Object::create() + * call. The object must be at the top of the Lua stack. + * @param leave_on_stack supplies whether to leave the object on the stack or not when the ref + * is constructed. + */ + LuaRef(const std::pair& object, bool leave_on_stack) { + reset(object, leave_on_stack); + } + + ~LuaRef() { unref(); } + T* get() { return object_.first; } + + /** + * Same as the LuaRef non-default constructor, but post-construction. + */ + void reset(const std::pair& object, bool leave_on_stack) { + unref(); + + if (leave_on_stack) { + lua_pushvalue(object.second, -1); + } + + object_ = object; + ref_ = luaL_ref(object_.second, LUA_REGISTRYINDEX); + ASSERT(ref_ != LUA_REFNIL); + } + + /** + * Return a LuaRef to its default/empty state. + */ + void reset() { + unref(); + object_ = std::pair{}; + ref_ = LUA_NOREF; + } + + /** + * Push the referenced object back onto the stack. + */ + void pushStack() { + ASSERT(object_.first); + lua_rawgeti(object_.second, LUA_REGISTRYINDEX, ref_); + } + +protected: + void unref() { + if (object_.second != nullptr) { + luaL_unref(object_.second, LUA_REGISTRYINDEX, ref_); + } + } + + std::pair object_; + int ref_; +}; + +/** + * This is a variant of LuaRef which also marks an object as dead during destruction. This is + * useful if an object should not be used after the scope of the pcall() or resume(). + * TODO(mattklein123): Add dedicated unit tests. This will require mocking a Lua state. + */ +template class LuaDeathRef : public LuaRef { +public: + ~LuaDeathRef() { markDead(); } + + void markDead() { + if (this->object_.first) { + this->object_.first->markDead(); + } + } + + void markLive() { + if (this->object_.first) { + this->object_.first->markLive(); + } + } + + void reset(const std::pair& object, bool leave_on_stack) { + markDead(); + LuaRef::reset(object, leave_on_stack); + } + + void reset() { + markDead(); + LuaRef::reset(); + } +}; + +} \ No newline at end of file diff --git a/envoy/main.cpp b/envoy/main.cpp new file mode 100644 index 0000000..2fe4d43 --- /dev/null +++ b/envoy/main.cpp @@ -0,0 +1,41 @@ +#include "envoy.h" + +int callLua(lua_State* state, const char *func) +{ + lua::LuaDeathRef handle; + lua_getglobal(state, func); + if (lua_isfunction(state, -1)) { + handle.reset(lua::StreamHandleWrapper::create(state), true); + lua_call(state, 1, 0); + } + lua_pop(state, 1); + return 0; +} + +int main(int argc, char *argv[]) +{ + if (argc < 1) { + printf("usage: envoy-apisix [lua file]\n"); + return 0; + } + + lua_State* state = luaL_newstate(); + luaL_openlibs(state); + + lua::BufferWrapper::registerType(state); + lua::HeaderMapIterator::registerType(state); + lua::HeaderMapWrapper::registerType(state); + lua::MetadataMapWrapper::registerType(state); + lua::MetadataMapIterator::registerType(state); + lua::StreamInfoWrapper::registerType(state); + lua::StreamHandleWrapper::registerType(state); + + if (luaL_dofile(state, argv[1])) { + printf("script load error: %s\n", lua_tostring(state, -1)); + return 0; + } + callLua(state, "envoy_on_request"); + + lua_close(state); + return 0; +} \ No newline at end of file diff --git a/envoy/packages.config b/envoy/packages.config new file mode 100644 index 0000000..0830d9e --- /dev/null +++ b/envoy/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/plugins/core.lua b/plugins/core.lua new file mode 100644 index 0000000..ede9254 --- /dev/null +++ b/plugins/core.lua @@ -0,0 +1,7 @@ + +return { + crypto = require("core.crypto"), + json = require("core.json"), + table = require("core.table"), + request = require("core.request"), +} \ No newline at end of file diff --git a/plugins/crypto.lua b/plugins/crypto.lua new file mode 100644 index 0000000..4bd2ee4 --- /dev/null +++ b/plugins/crypto.lua @@ -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 \ No newline at end of file diff --git a/plugins/ctx.lua b/plugins/ctx.lua new file mode 100644 index 0000000..d5d76d4 --- /dev/null +++ b/plugins/ctx.lua @@ -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 diff --git a/plugins/entry.lua b/plugins/entry.lua new file mode 100644 index 0000000..8540abc --- /dev/null +++ b/plugins/entry.lua @@ -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 \ No newline at end of file diff --git a/plugins/envoy.yaml b/plugins/envoy.yaml new file mode 100644 index 0000000..23aaa92 --- /dev/null +++ b/plugins/envoy.yaml @@ -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 diff --git a/plugins/json.lua b/plugins/json.lua new file mode 100644 index 0000000..54d4448 --- /dev/null +++ b/plugins/json.lua @@ -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 \ No newline at end of file diff --git a/plugins/log.lua b/plugins/log.lua new file mode 100644 index 0000000..6df7ef8 --- /dev/null +++ b/plugins/log.lua @@ -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 \ No newline at end of file diff --git a/plugins/plugin.lua b/plugins/plugin.lua new file mode 100644 index 0000000..e83c4d8 --- /dev/null +++ b/plugins/plugin.lua @@ -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 diff --git a/plugins/plugins/basic-auth.lua b/plugins/plugins/basic-auth.lua new file mode 100644 index 0000000..2404faa --- /dev/null +++ b/plugins/plugins/basic-auth.lua @@ -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 diff --git a/plugins/plugins/ldap-auth.lua b/plugins/plugins/ldap-auth.lua new file mode 100644 index 0000000..dbab6da --- /dev/null +++ b/plugins/plugins/ldap-auth.lua @@ -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 diff --git a/plugins/plugins/redirect.lua b/plugins/plugins/redirect.lua new file mode 100644 index 0000000..ef7b7be --- /dev/null +++ b/plugins/plugins/redirect.lua @@ -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 diff --git a/plugins/plugins/user-code.lua b/plugins/plugins/user-code.lua new file mode 100644 index 0000000..9bfc5ec --- /dev/null +++ b/plugins/plugins/user-code.lua @@ -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 diff --git a/plugins/request.lua b/plugins/request.lua new file mode 100644 index 0000000..ed75175 --- /dev/null +++ b/plugins/request.lua @@ -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 \ No newline at end of file diff --git a/plugins/string.lua b/plugins/string.lua new file mode 100644 index 0000000..952152a --- /dev/null +++ b/plugins/string.lua @@ -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 diff --git a/plugins/table.lua b/plugins/table.lua new file mode 100644 index 0000000..4ba1229 --- /dev/null +++ b/plugins/table.lua @@ -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