diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 1024b02cd..e28183ee6 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -19,8 +19,8 @@ jobs: - name: build run: | sudo apt update - sudo apt install libssl-dev libnghttp2-dev - ./configure --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis + sudo apt install libssl-dev libnghttp2-dev liblua5.4-dev + ./configure --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis --with-lua make libhv evpp - name: test diff --git a/.gitignore b/.gitignore index 5822f761f..eb155d1da 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,9 @@ CMakeCache.txt cmake_install.cmake _codeql_build_dir/ _codeql_detected_source_root + +# agent +.claude/ +.superpowers/ +specs/ +docs/superpowers/ diff --git a/CMakeLists.txt b/CMakeLists.txt index ffbe683fe..d64326ae6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,8 @@ option(WITH_OPENSSL "with openssl library" OFF) option(WITH_GNUTLS "with gnutls library" OFF) option(WITH_MBEDTLS "with mbedtls library" OFF) +option(WITH_LUA "with lua library" OFF) + option(WITH_KCP "compile event/kcp" OFF) if(CMAKE_SYSTEM_NAME MATCHES "Linux") @@ -180,6 +182,17 @@ if(WITH_IO_URING) set(LIBS ${LIBS} uring) endif() +if(WITH_LUA) + add_definitions(-DWITH_LUA) + find_package(Lua) + if(LUA_FOUND) + include_directories(${LUA_INCLUDE_DIR}) + set(LIBS ${LIBS} ${LUA_LIBRARIES}) + else() + set(LIBS ${LIBS} lua) + endif() +endif() + if(WIN32 OR MINGW) add_definitions(-DWIN32_LEAN_AND_MEAN -D_CRT_SECURE_NO_WARNINGS -D_WIN32_WINNT=0x0600) set(LIBS ${LIBS} secur32 crypt32 winmm iphlpapi ws2_32) @@ -238,6 +251,9 @@ if(WITH_EVPP) endif() if(WITH_HTTP_SERVER) set(LIBHV_HEADERS ${LIBHV_HEADERS} ${HTTP_SERVER_HEADERS}) + if(WITH_LUA) + set(LIBHV_HEADERS ${LIBHV_HEADERS} http/server/HttpScriptHandler.h http/server/HttpLuaHandler.h) + endif() set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} http/server) endif() if(WITH_HTTP_CLIENT) diff --git a/Makefile b/Makefile index 2eafb2a15..6eec0d1d2 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,9 @@ endif ifeq ($(WITH_HTTP_SERVER), yes) LIBHV_HEADERS += $(HTTP_SERVER_HEADERS) LIBHV_SRCDIRS += http/server +ifeq ($(WITH_LUA), yes) +LIBHV_HEADERS += http/server/HttpScriptHandler.h http/server/HttpLuaHandler.h +endif endif ifeq ($(WITH_HTTP_CLIENT), yes) @@ -315,6 +318,12 @@ unittest: prepare ifeq ($(WITH_EVPP), yes) $(MAKE) libhv $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -o bin/tcpclient_dns_test unittest/tcpclient_dns_test.cpp -Llib -lhv -pthread +ifeq ($(WITH_LUA), yes) + $(MAKE) libhv + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_LUA $(LUA_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/server -o bin/http_lua_handler_test unittest/http_lua_handler_test.cpp -Llib -lhv -pthread $(LUA_LIBS) +else + $(RM) bin/http_lua_handler_test +endif ifeq ($(WITH_REDIS), yes) $(MAKE) libhv $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Ievent -Icpputil -Iredis -o bin/redis_protocol_test unittest/redis_protocol_test.cpp redis/RedisMessage.cpp diff --git a/Makefile.in b/Makefile.in index 1b807ed38..6c9b4392b 100644 --- a/Makefile.in +++ b/Makefile.in @@ -22,6 +22,7 @@ # LIBS="opencv_core opencv_highgui" #+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -include config.mk +-include Makefile.vars # VARIABLES TARGET ?= test @@ -160,6 +161,11 @@ ifeq ($(USE_MULTIMAP), yes) CPPFLAGS += -DUSE_MULTIMAP endif +ifeq ($(WITH_LUA), yes) + CPPFLAGS += -DWITH_LUA $(LUA_CFLAGS) + LDFLAGS += $(LUA_LIBS) +endif + CPPFLAGS += $(addprefix -D, $(DEFINES)) CPPFLAGS += $(addprefix -I, $(INCDIRS)) CPPFLAGS += $(addprefix -I, $(SRCDIRS)) diff --git a/Makefile.vars b/Makefile.vars index e3cb3121e..935ba5492 100644 --- a/Makefile.vars +++ b/Makefile.vars @@ -7,6 +7,13 @@ PREFIX ?= /usr/local INSTALL_INCDIR ?= $(PREFIX)/include/hv INSTALL_LIBDIR ?= $(PREFIX)/lib +PKG_CONFIG ?= pkg-config +LUA_PKG_CONFIG ?= $(shell if command -v $(PKG_CONFIG) >/dev/null 2>&1 && $(PKG_CONFIG) --exists lua; then echo lua; fi) +LUA_PREFIX ?= $(shell for dir in /opt/homebrew/opt/lua /usr/local/opt/lua /usr; do if [ -f "$$dir/include/lua/lua.h" ] || [ -f "$$dir/include/lua.h" ]; then echo $$dir; break; fi; done) +LUA_INCLUDE_DIR ?= $(shell if [ -n "$(LUA_PREFIX)" ]; then for dir in "$(LUA_PREFIX)/include/lua" "$(LUA_PREFIX)/include/lua5.5" "$(LUA_PREFIX)/include/lua5.4" "$(LUA_PREFIX)/include/lua5.3" "$(LUA_PREFIX)/include"; do if [ -f "$$dir/lua.h" ]; then echo $$dir; break; fi; done; fi) +LUA_CFLAGS ?= $(shell if [ -n "$(LUA_PKG_CONFIG)" ]; then $(PKG_CONFIG) --cflags $(LUA_PKG_CONFIG); elif [ -n "$(LUA_INCLUDE_DIR)" ]; then echo -I$(LUA_INCLUDE_DIR); fi) +LUA_LIBS ?= $(shell if [ -n "$(LUA_PKG_CONFIG)" ]; then $(PKG_CONFIG) --libs $(LUA_PKG_CONFIG); elif [ -n "$(LUA_PREFIX)" ]; then echo -L$(LUA_PREFIX)/lib -llua; else echo -llua; fi) + BASE_HEADERS = base/hplatform.h\ \ base/hdef.h\ diff --git a/config.ini b/config.ini index 466d3b936..9b2a0f642 100644 --- a/config.ini +++ b/config.ini @@ -36,6 +36,8 @@ WITH_NGHTTP2=no WITH_OPENSSL=no WITH_GNUTLS=no WITH_MBEDTLS=no +# for http lua handler +WITH_LUA=no # rudp WITH_KCP=no diff --git a/config.mk b/config.mk index 4283f5a71..403d97628 100644 --- a/config.mk +++ b/config.mk @@ -10,7 +10,7 @@ WITH_HTTP=yes WITH_HTTP_SERVER=yes WITH_HTTP_CLIENT=yes WITH_MQTT=no -WITH_REDIS=yes +WITH_REDIS=no ENABLE_UDS=no ENABLE_WINDUMP=no USE_MULTIMAP=no @@ -19,6 +19,7 @@ WITH_NGHTTP2=no WITH_OPENSSL=no WITH_GNUTLS=no WITH_MBEDTLS=no +WITH_LUA=no WITH_KCP=no WITH_IO_URING=no -CONFIG_DATE=20260711 \ No newline at end of file +CONFIG_DATE=20260728 diff --git a/docs/PLAN.md b/docs/PLAN.md index 97004d020..badb081fb 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -10,6 +10,7 @@ - mqtt client - redis client - async DNS +- http lua handler ## Plan diff --git a/docs/cn/HttpLuaHandler.md b/docs/cn/HttpLuaHandler.md new file mode 100644 index 000000000..c7293c81f --- /dev/null +++ b/docs/cn/HttpLuaHandler.md @@ -0,0 +1,140 @@ +# Http Lua Handler + +`HttpScriptHandler` 允许 `HttpService` 调用脚本里的 `handle(ctx)` 方法处理 HTTP 请求。当前支持 `.lua` 脚本,适合把少量业务逻辑从 C++ 编译周期里解耦出来:修改脚本后无需重新编译服务,下一次请求会自动加载新脚本。 + +该功能是可选模块,默认不编译。 + +## 编译 + +需要 Lua 5.3 或更新版本的开发库。 + +Makefile: + +```bash +make libhv WITH_LUA=yes +make http_server_test WITH_LUA=yes +make unittest WITH_LUA=yes +``` + +如果系统没有 `pkg-config`,或者 Lua 安装在自定义路径,可以显式指定: + +```bash +make libhv WITH_LUA=yes \ + LUA_CFLAGS="-I/usr/local/include/lua" \ + LUA_LIBS="-L/usr/local/lib -llua" +``` + +CMake: + +```bash +cmake -S . -B build -DWITH_LUA=ON -DBUILD_EXAMPLES=ON -DBUILD_UNITTEST=ON +cmake --build build +``` + +## 基本用法 + +C++: + +```cpp +#include "HttpServer.h" +#include "HttpScriptHandler.h" + +using namespace hv; + +int main() { + HttpService router; + router.GET("/hello", HttpScriptHandler("scripts/hello.lua")); + + HttpServer server; + server.port = 8080; + server.service = &router; + server.run(); + return 0; +} +``` + +Lua: + +```lua +function handle(ctx) + local id = ctx:query("id", "") + return ctx:json({ + ok = true, + id = id, + path = ctx:path() + }) +end +``` + +如果需要明确指定 Lua 引擎,也可以直接使用 `HttpLuaHandler("scripts/hello.lua")`。推荐用户代码优先使用 `HttpScriptHandler`,这样后续增加 JS/Python 等脚本引擎时不用改路由注册代码。 + +## 目录映射 + +`HttpService::Script(path, script_dir)` 可以把 URL 前缀映射到脚本目录,内部同样使用 `HttpScriptHandler`: + +```cpp +router.Script("/script/", "scripts"); +``` + +访问 `/script/user?id=42` 时会调用 `scripts/user.lua`。访问 `/script/` 时会调用 `scripts/index.lua`。当前目录映射只自动补 `.lua` 后缀。 + +目录映射默认支持 `GET`、`POST`、`PUT`、`DELETE`、`PATCH`。路径中包含 `..` 路径段时返回 `403`。 + +## ctx API + +首版只暴露 HTTP handler 必需的最小 API: + +```lua +ctx:method() -- GET/POST/... +ctx:path() -- URL path +ctx:param(name, default) -- query/restful params +ctx:query(name, default) -- alias of param +ctx:header(name, default) +ctx:body() + +ctx:status(code) +ctx:set_header(name, value) +ctx:text(str) +ctx:json(table) +``` + +`handle(ctx)` 可以直接调用 `ctx:text` / `ctx:json` 返回,也可以返回字符串、数字状态码或 Lua table: + +```lua +function handle(ctx) + ctx:status(201) + ctx:set_header("X-From", "lua") + return ctx:text("created") +end +``` + +## hv API + +首版只提供少量宿主能力: + +```lua +hv.log(...) +hv.now() +``` + +暂不暴露 `hv.event_loop`、TCP/HTTP client、Redis 等能力,避免脚本直接操作底层事件循环。后续可以按业务需要增加受控的 `hv.redis`、`hv.http` 等模块。 + +## 热更新 + +`HttpScriptHandler` 当前会把 `.lua` 文件转给 `HttpLuaHandler`。`HttpLuaHandler` 会记录脚本文件的 `mtime`。每次请求前,如果文件被修改,会重新加载脚本。 + +重新加载失败时: + +- 如果已有旧版本脚本,继续使用旧版本。 +- 如果首次加载失败,返回 `500`。 + +这能避免线上脚本语法错误直接打断已有服务。 + +## 示例 + +```bash +make http_server_test WITH_LUA=yes +bin/http_server_test 8080 +curl "http://127.0.0.1:8080/lua/hello?id=42" +curl "http://127.0.0.1:8080/script/hello?id=42" +``` diff --git a/docs/cn/README.md b/docs/cn/README.md index 7f4c8d3a2..db22adb57 100644 --- a/docs/cn/README.md +++ b/docs/cn/README.md @@ -14,6 +14,7 @@ - [class UdpServer: UDP服务端类](UdpServer.md) - [class UdpClient: UDP客户端类](UdpClient.md) - [class HttpServer: HTTP服务端类](HttpServer.md) +- [Http Lua Handler: HTTP Lua脚本处理器](HttpLuaHandler.md) - [class HttpClient: HTTP客户端类](HttpClient.md) - [class WebSocketServer: WebSocket服务端类](WebSocketServer.md) - [class WebSocketClient: WebSocket客户端类](WebSocketClient.md) diff --git a/examples/http_server_test.cpp b/examples/http_server_test.cpp index 0adb3aee4..4782b33a3 100644 --- a/examples/http_server_test.cpp +++ b/examples/http_server_test.cpp @@ -8,6 +8,10 @@ #include "hthread.h" // import hv_gettid #include "hasync.h" // import hv::async +#ifdef WITH_LUA +#include "HttpScriptHandler.h" +#endif + using namespace hv; /* @@ -88,6 +92,13 @@ int main(int argc, char** argv) { return ctx->send(resp.dump(2)); }); +#ifdef WITH_LUA + // curl -v "http://ip:port/lua/hello?id=42" + router.GET("/lua/hello", HttpScriptHandler("examples/scripts/hello.lua")); + // curl -v "http://ip:port/script/hello?id=42" + router.Script("/script/", "examples/scripts"); +#endif + // curl -v http://ip:port/async router.GET("/async", [](const HttpRequestPtr& req, const HttpResponseWriterPtr& writer) { writer->Begin(); diff --git a/examples/scripts/hello.lua b/examples/scripts/hello.lua new file mode 100644 index 000000000..a77d429fe --- /dev/null +++ b/examples/scripts/hello.lua @@ -0,0 +1,23 @@ +function get(ctx) + hv.log("lua get", ctx:path()) + return ctx:json({ + ok = true, + method = "GET", + path = ctx:path(), + id = ctx:query("id", "") + }) +end + +function post(ctx) + hv.log("lua post", ctx:path()) + return ctx:text("POST " .. ctx:body()) +end + +function handle(ctx) + hv.log("lua fallback", ctx:method(), ctx:path()) + return ctx:json({ + ok = true, + method = ctx:method(), + path = ctx:path() + }) +end diff --git a/hconfig.h.in b/hconfig.h.in index 4880666a8..2bef47660 100644 --- a/hconfig.h.in +++ b/hconfig.h.in @@ -100,4 +100,6 @@ #cmakedefine WITH_KCP 1 #cmakedefine WITH_IO_URING 1 +#cmakedefine WITH_LUA 1 + #endif // HV_CONFIG_H_ diff --git a/http/server/HttpLuaHandler.cpp b/http/server/HttpLuaHandler.cpp new file mode 100644 index 000000000..85a6268d2 --- /dev/null +++ b/http/server/HttpLuaHandler.cpp @@ -0,0 +1,425 @@ +#include "HttpLuaHandler.h" + +#ifdef WITH_LUA + +#include +#include + +extern "C" { +#include +#include +#include +} + +#include "hfile.h" +#include "hlog.h" +#include "hpath.h" +#include "hstring.h" +#include "htime.h" + +namespace hv { + +namespace { + +static const char* LUA_CTX_META = "hv.HttpContext"; + +struct LuaHttpContext { + HttpContextPtr ctx; +}; + +static LuaHttpContext* lua_check_ctx(lua_State* L) { + return (LuaHttpContext*)luaL_checkudata(L, 1, LUA_CTX_META); +} + +static std::string lua_opt_string_arg(lua_State* L, int index, const char* defvalue = "") { + if (lua_isnoneornil(L, index)) { + return defvalue; + } + size_t len = 0; + const char* s = luaL_checklstring(L, index, &len); + return std::string(s, len); +} + +static int lua_ctx_method(lua_State* L) { + LuaHttpContext* holder = lua_check_ctx(L); + lua_pushstring(L, http_method_str(holder->ctx->request->method)); + return 1; +} + +static int lua_ctx_path(lua_State* L) { + LuaHttpContext* holder = lua_check_ctx(L); + lua_pushlstring(L, holder->ctx->request->path.data(), holder->ctx->request->path.size()); + return 1; +} + +static int lua_ctx_param(lua_State* L) { + LuaHttpContext* holder = lua_check_ctx(L); + std::string key = lua_opt_string_arg(L, 2); + std::string defvalue = lua_opt_string_arg(L, 3); + lua_pushstring(L, holder->ctx->request->GetParam(key.c_str(), defvalue).c_str()); + return 1; +} + +static int lua_ctx_query(lua_State* L) { + return lua_ctx_param(L); +} + +static int lua_ctx_header(lua_State* L) { + LuaHttpContext* holder = lua_check_ctx(L); + std::string key = lua_opt_string_arg(L, 2); + std::string defvalue = lua_opt_string_arg(L, 3); + lua_pushstring(L, holder->ctx->request->GetHeader(key.c_str(), defvalue).c_str()); + return 1; +} + +static int lua_ctx_body(lua_State* L) { + LuaHttpContext* holder = lua_check_ctx(L); + const std::string& body = holder->ctx->request->body; + lua_pushlstring(L, body.data(), body.size()); + return 1; +} + +static int lua_ctx_status(lua_State* L) { + LuaHttpContext* holder = lua_check_ctx(L); + int status = (int)luaL_checkinteger(L, 2); + holder->ctx->response->status_code = (http_status)status; + lua_pushinteger(L, status); + return 1; +} + +static int lua_ctx_set_header(lua_State* L) { + LuaHttpContext* holder = lua_check_ctx(L); + std::string key = lua_opt_string_arg(L, 2); + std::string value = lua_opt_string_arg(L, 3); + holder->ctx->response->SetHeader(key.c_str(), value); + if (stricmp(key.c_str(), "Content-Type") == 0) { + holder->ctx->response->SetContentType(value.c_str()); + } + return 0; +} + +static int lua_ctx_text(lua_State* L) { + LuaHttpContext* holder = lua_check_ctx(L); + std::string text = lua_opt_string_arg(L, 2); + holder->ctx->response->String(text); + lua_pushinteger(L, holder->ctx->response->status_code); + return 1; +} + +static Json lua_to_json(lua_State* L, int index); + +static Json lua_table_to_json(lua_State* L, int index) { + index = lua_absindex(L, index); + bool is_array = true; + lua_Integer max_index = 0; + size_t count = 0; + + lua_pushnil(L); + while (lua_next(L, index) != 0) { + ++count; + if (lua_type(L, -2) == LUA_TNUMBER && lua_isinteger(L, -2)) { + lua_Integer k = lua_tointeger(L, -2); + if (k <= 0) { + is_array = false; + } else if (k > max_index) { + max_index = k; + } + } else { + is_array = false; + } + lua_pop(L, 1); + } + + if (is_array && (lua_Integer)count == max_index) { + Json j = Json::array(); + for (lua_Integer i = 1; i <= max_index; ++i) { + lua_geti(L, index, i); + j.push_back(lua_to_json(L, -1)); + lua_pop(L, 1); + } + return j; + } + + Json j = Json::object(); + lua_pushnil(L); + while (lua_next(L, index) != 0) { + std::string key; + if (lua_type(L, -2) == LUA_TSTRING) { + size_t len = 0; + const char* s = lua_tolstring(L, -2, &len); + key.assign(s, len); + } else if (lua_type(L, -2) == LUA_TNUMBER) { + key = hv::to_string((int64_t)lua_tointeger(L, -2)); + } + if (!key.empty()) { + j[key] = lua_to_json(L, -1); + } + lua_pop(L, 1); + } + return j; +} + +static Json lua_to_json(lua_State* L, int index) { + switch (lua_type(L, index)) { + case LUA_TNIL: + return nullptr; + case LUA_TBOOLEAN: + return lua_toboolean(L, index) != 0; + case LUA_TNUMBER: + if (lua_isinteger(L, index)) { + return (int64_t)lua_tointeger(L, index); + } + return lua_tonumber(L, index); + case LUA_TSTRING: { + size_t len = 0; + const char* s = lua_tolstring(L, index, &len); + return std::string(s, len); + } + case LUA_TTABLE: + return lua_table_to_json(L, index); + default: + return nullptr; + } +} + +static int lua_ctx_json(lua_State* L) { + LuaHttpContext* holder = lua_check_ctx(L); + Json j = lua_to_json(L, 2); + holder->ctx->response->Json(j); + lua_pushinteger(L, holder->ctx->response->status_code); + return 1; +} + +static int lua_ctx_gc(lua_State* L) { + LuaHttpContext* holder = lua_check_ctx(L); + holder->~LuaHttpContext(); + return 0; +} + +static void lua_push_ctx(lua_State* L, const HttpContextPtr& ctx) { + void* storage = lua_newuserdata(L, sizeof(LuaHttpContext)); + new (storage) LuaHttpContext(); + ((LuaHttpContext*)storage)->ctx = ctx; + luaL_getmetatable(L, LUA_CTX_META); + lua_setmetatable(L, -2); +} + +static int lua_hv_log(lua_State* L) { + int n = lua_gettop(L); + std::string line; + for (int i = 1; i <= n; ++i) { + size_t len = 0; + const char* s = luaL_tolstring(L, i, &len); + if (i > 1) line += "\t"; + line.append(s, len); + lua_pop(L, 1); + } + hlogi("[lua] %s", line.c_str()); + return 0; +} + +static int lua_hv_now(lua_State* L) { + lua_pushinteger(L, (lua_Integer)time(NULL)); + return 1; +} + +static void register_ctx(lua_State* L) { + if (luaL_newmetatable(L, LUA_CTX_META)) { + lua_pushcfunction(L, lua_ctx_gc); + lua_setfield(L, -2, "__gc"); + + lua_newtable(L); + lua_pushcfunction(L, lua_ctx_method); + lua_setfield(L, -2, "method"); + lua_pushcfunction(L, lua_ctx_path); + lua_setfield(L, -2, "path"); + lua_pushcfunction(L, lua_ctx_param); + lua_setfield(L, -2, "param"); + lua_pushcfunction(L, lua_ctx_query); + lua_setfield(L, -2, "query"); + lua_pushcfunction(L, lua_ctx_header); + lua_setfield(L, -2, "header"); + lua_pushcfunction(L, lua_ctx_body); + lua_setfield(L, -2, "body"); + lua_pushcfunction(L, lua_ctx_status); + lua_setfield(L, -2, "status"); + lua_pushcfunction(L, lua_ctx_set_header); + lua_setfield(L, -2, "set_header"); + lua_pushcfunction(L, lua_ctx_text); + lua_setfield(L, -2, "text"); + lua_pushcfunction(L, lua_ctx_json); + lua_setfield(L, -2, "json"); + lua_setfield(L, -2, "__index"); + } + lua_pop(L, 1); +} + +static void register_hv(lua_State* L) { + lua_newtable(L); + lua_pushcfunction(L, lua_hv_log); + lua_setfield(L, -2, "log"); + lua_pushcfunction(L, lua_hv_now); + lua_setfield(L, -2, "now"); + lua_setglobal(L, "hv"); +} + +static time_t file_mtime(const std::string& filepath) { + struct stat st; + if (stat(filepath.c_str(), &st) != 0) { + return 0; + } + return st.st_mtime; +} + +} // namespace + +HttpLuaHandler::HttpLuaHandler(const char* filepath, const HttpLuaHandlerOptions& options) + : filepath_(filepath ? filepath : "") + , options_(options) + , L_(NULL) + , mtime_(0) { +} + +HttpLuaHandler::HttpLuaHandler(const HttpLuaHandler& rhs) + : filepath_(rhs.filepath_) + , options_(rhs.options_) + , L_(NULL) + , mtime_(0) { +} + +HttpLuaHandler& HttpLuaHandler::operator=(const HttpLuaHandler& rhs) { + if (this == &rhs) return *this; + std::lock_guard lock(mutex_); + closeLocked(); + filepath_ = rhs.filepath_; + options_ = rhs.options_; + mtime_ = 0; + last_error_.clear(); + return *this; +} + +HttpLuaHandler::~HttpLuaHandler() { + std::lock_guard lock(mutex_); + closeLocked(); +} + +std::string HttpLuaHandler::lastError() const { + std::lock_guard lock(mutex_); + return last_error_; +} + +void HttpLuaHandler::setErrorLocked(const std::string& error) { + last_error_ = error; +} + +void HttpLuaHandler::closeLocked() { + if (L_) { + lua_close(L_); + L_ = NULL; + } +} + +bool HttpLuaHandler::loadLocked(time_t mtime) { + lua_State* L = luaL_newstate(); + if (L == NULL) { + setErrorLocked("luaL_newstate failed"); + return false; + } + + luaL_openlibs(L); + register_ctx(L); + register_hv(L); + + if (luaL_loadfile(L, filepath_.c_str()) != LUA_OK || lua_pcall(L, 0, 0, 0) != LUA_OK) { + std::string error = lua_tostring(L, -1) ? lua_tostring(L, -1) : "load script failed"; + lua_close(L); + setErrorLocked(error); + hloge("load lua script %s failed: %s", filepath_.c_str(), error.c_str()); + return false; + } + + lua_getglobal(L, "handle"); + if (!lua_isfunction(L, -1)) { + lua_close(L); + setErrorLocked("global handle(ctx) is not a function"); + hloge("load lua script %s failed: handle(ctx) not found", filepath_.c_str()); + return false; + } + lua_pop(L, 1); + + closeLocked(); + L_ = L; + mtime_ = mtime; + last_error_.clear(); + return true; +} + +bool HttpLuaHandler::reloadIfNeeded() { + std::lock_guard lock(mutex_); + time_t mtime = file_mtime(filepath_); + if (mtime == 0) { + setErrorLocked(strerror(errno)); + return L_ != NULL; + } + if (L_ != NULL && (!options_.reload_on_change || mtime == mtime_)) { + return true; + } + return loadLocked(mtime) || L_ != NULL; +} + +int HttpLuaHandler::callLocked(const HttpContextPtr& ctx) { + std::string handler_name = http_method_str(ctx->request->method); + tolower(handler_name); + lua_getglobal(L_, handler_name.c_str()); + if (!lua_isfunction(L_, -1)) { + lua_pop(L_, 1); + lua_getglobal(L_, "handle"); + } + lua_push_ctx(L_, ctx); + if (lua_pcall(L_, 1, 1, 0) != LUA_OK) { + std::string error = lua_tostring(L_, -1) ? lua_tostring(L_, -1) : "call handle failed"; + lua_pop(L_, 1); + setErrorLocked(error); + hloge("call lua script %s failed: %s", filepath_.c_str(), error.c_str()); + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String(error); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + + int status = ctx->response->status_code; + if (lua_isinteger(L_, -1)) { + status = (int)lua_tointeger(L_, -1); + if (ctx->response->status_code == HTTP_STATUS_OK) { + ctx->response->status_code = (http_status)status; + } + } else if (lua_isstring(L_, -1)) { + size_t len = 0; + const char* s = lua_tolstring(L_, -1, &len); + ctx->response->String(std::string(s, len)); + status = ctx->response->status_code; + } else if (lua_istable(L_, -1)) { + Json j = lua_to_json(L_, -1); + ctx->response->Json(j); + status = ctx->response->status_code; + } + lua_pop(L_, 1); + return status; +} + +int HttpLuaHandler::operator()(const HttpContextPtr& ctx) { + if (!ctx || !ctx->response || !ctx->request) { + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + if (!reloadIfNeeded()) { + std::lock_guard lock(mutex_); + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String(last_error_); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + std::lock_guard lock(mutex_); + return callLocked(ctx); +} + +} // namespace hv + +#endif // WITH_LUA diff --git a/http/server/HttpLuaHandler.h b/http/server/HttpLuaHandler.h new file mode 100644 index 000000000..305c96c9c --- /dev/null +++ b/http/server/HttpLuaHandler.h @@ -0,0 +1,59 @@ +#ifndef HV_HTTP_LUA_HANDLER_H_ +#define HV_HTTP_LUA_HANDLER_H_ + +#include +#include +#include +#include + +#include "hexport.h" +#include "HttpService.h" + +struct lua_State; + +namespace hv { + +struct HV_EXPORT HttpLuaHandlerOptions { + bool reload_on_change; + + HttpLuaHandlerOptions() { + reload_on_change = true; + } +}; + +class HV_EXPORT HttpLuaHandler { +public: + HttpLuaHandler(const char* filepath, const HttpLuaHandlerOptions& options = HttpLuaHandlerOptions()); + HttpLuaHandler(const HttpLuaHandler& rhs); + HttpLuaHandler& operator=(const HttpLuaHandler& rhs); + ~HttpLuaHandler(); + + int operator()(const HttpContextPtr& ctx); + + const std::string& filepath() const { + return filepath_; + } + + std::string lastError() const; + +private: + bool reloadIfNeeded(); + bool loadLocked(time_t mtime); + void closeLocked(); + int callLocked(const HttpContextPtr& ctx); + void setErrorLocked(const std::string& error); + +private: + std::string filepath_; + HttpLuaHandlerOptions options_; + lua_State* L_; + time_t mtime_; + std::string last_error_; + mutable std::mutex mutex_; +}; + +typedef std::shared_ptr HttpLuaHandlerPtr; + +} // namespace hv + +#endif // HV_HTTP_LUA_HANDLER_H_ diff --git a/http/server/HttpScriptHandler.cpp b/http/server/HttpScriptHandler.cpp new file mode 100644 index 000000000..83ee2bae7 --- /dev/null +++ b/http/server/HttpScriptHandler.cpp @@ -0,0 +1,65 @@ +#include "HttpScriptHandler.h" + +#ifdef WITH_LUA + +#include "hbase.h" +#include "hstring.h" +#include "HttpLuaHandler.h" +#include + +namespace hv { + +struct HttpScriptHandler::State { + std::once_flag lua_once; + HttpLuaHandlerPtr lua_handler; +}; + +namespace { + +static bool filepath_has_suffix(const std::string& filepath, const char* suffix) { + std::string ext = hv_suffixname(filepath.c_str()); + return stricmp(ext.c_str(), suffix) == 0; +} + +} // namespace + +HttpScriptHandler::HttpScriptHandler(const char* filepath, const HttpScriptHandlerOptions& options) + : filepath_(filepath ? filepath : "") + , options_(options) + , state_(std::make_shared()) { +} + +HttpScriptHandler::HttpScriptHandler(const HttpScriptHandler& rhs) + : filepath_(rhs.filepath_) + , options_(rhs.options_) + , state_(std::make_shared()) { +} + +HttpScriptHandler& HttpScriptHandler::operator=(const HttpScriptHandler& rhs) { + if (this == &rhs) return *this; + filepath_ = rhs.filepath_; + options_ = rhs.options_; + state_ = std::make_shared(); + return *this; +} + +int HttpScriptHandler::operator()(const HttpContextPtr& ctx) { + if (filepath_has_suffix(filepath_, "lua")) { + std::call_once(state_->lua_once, [this]() { + HttpLuaHandlerOptions lua_options; + lua_options.reload_on_change = options_.reload_on_change; + state_->lua_handler = std::make_shared(filepath_.c_str(), lua_options); + }); + return (*state_->lua_handler)(ctx); + } + + if (ctx && ctx->response) { + ctx->response->status_code = HTTP_STATUS_NOT_IMPLEMENTED; + ctx->response->String("unsupported script type"); + } + return HTTP_STATUS_NOT_IMPLEMENTED; +} + +} // namespace hv + +#endif // WITH_LUA diff --git a/http/server/HttpScriptHandler.h b/http/server/HttpScriptHandler.h new file mode 100644 index 000000000..60b247c5c --- /dev/null +++ b/http/server/HttpScriptHandler.h @@ -0,0 +1,44 @@ +#ifndef HV_HTTP_SCRIPT_HANDLER_H_ +#define HV_HTTP_SCRIPT_HANDLER_H_ + +#include +#include + +#include "hexport.h" +#include "HttpContext.h" + +namespace hv { + +struct HV_EXPORT HttpScriptHandlerOptions { + bool reload_on_change; + + HttpScriptHandlerOptions() { + reload_on_change = true; + } +}; + +class HV_EXPORT HttpScriptHandler { +public: + HttpScriptHandler(const char* filepath, const HttpScriptHandlerOptions& options = HttpScriptHandlerOptions()); + HttpScriptHandler(const HttpScriptHandler& rhs); + HttpScriptHandler& operator=(const HttpScriptHandler& rhs); + + int operator()(const HttpContextPtr& ctx); + + const std::string& filepath() const { + return filepath_; + } + +private: + struct State; + + std::string filepath_; + HttpScriptHandlerOptions options_; + std::shared_ptr state_; +}; + +typedef std::shared_ptr HttpScriptHandlerPtr; + +} // namespace hv + +#endif // HV_HTTP_SCRIPT_HANDLER_H_ diff --git a/http/server/HttpService.cpp b/http/server/HttpService.cpp index 896113682..da7b946c4 100644 --- a/http/server/HttpService.cpp +++ b/http/server/HttpService.cpp @@ -1,6 +1,13 @@ #include "HttpService.h" #include "HttpMiddleware.h" #include "HttpRouter.h" +#ifdef WITH_LUA +#include "HttpScriptHandler.h" +#include "hpath.h" +#include "hstring.h" +#include +#include +#endif namespace hv { @@ -108,6 +115,59 @@ std::string HttpService::GetStaticFilepath(const char* path) { return filepath; } +#ifdef WITH_LUA +void HttpService::Script(const char* path, const char* script_dir) { + std::string route_path(path ? path : ""); + if (route_path.empty()) return; + if (route_path.back() != '*') { + if (route_path.back() != '/') route_path += '/'; + route_path += '*'; + } + + std::string route_prefix = route_path.substr(0, route_path.size() - 1); + std::string root(script_dir ? script_dir : ""); + while (!root.empty() && (root.back() == '/' || root.back() == '\\')) { + root.pop_back(); + } + + std::shared_ptr > scripts = + std::make_shared >(); + std::shared_ptr scripts_mutex = std::make_shared(); + http_ctx_handler script_handler_func = [route_prefix, root, scripts, scripts_mutex](const HttpContextPtr& ctx) -> int { + std::string path = ctx->path(); + if (path.compare(0, route_prefix.size(), route_prefix) != 0) { + return HTTP_STATUS_NOT_FOUND; + } + std::string name = path.substr(route_prefix.size()); + if (name.empty()) { + name = "index"; + } + if (strstr(path.c_str(), "/..") || strstr(path.c_str(), "\\..")) { + return HTTP_STATUS_FORBIDDEN; + } + std::string script = HPath::join(root, name); + if (HPath::suffixname(script).empty()) { + script += ".lua"; + } + + HttpScriptHandlerPtr script_handler; + { + std::lock_guard lock(*scripts_mutex); + auto iter = scripts->find(script); + if (iter == scripts->end()) { + script_handler = std::make_shared(script.c_str()); + (*scripts)[script] = script_handler; + } else { + script_handler = iter->second; + } + } + return (*script_handler)(ctx); + }; + + Any(route_path.c_str(), http_handler(script_handler_func)); +} +#endif + void HttpService::Proxy(const char* path, const char* url) { proxies[path] = url; } diff --git a/http/server/HttpService.h b/http/server/HttpService.h index 7b9686bc5..37073d637 100644 --- a/http/server/HttpService.h +++ b/http/server/HttpService.h @@ -56,12 +56,8 @@ struct http_handler { http_handler(http_async_handler fn) : async_handler(std::move(fn)) {} http_handler(http_ctx_handler fn) : ctx_handler(std::move(fn)) {} http_handler(http_state_handler fn) : state_handler(std::move(fn)) {} - http_handler(const http_handler& rhs) - : sync_handler(std::move(const_cast(rhs).sync_handler)) - , async_handler(std::move(const_cast(rhs).async_handler)) - , ctx_handler(std::move(const_cast(rhs).ctx_handler)) - , state_handler(std::move(const_cast(rhs).state_handler)) - {} + http_handler(const http_handler& rhs) = default; + http_handler& operator=(const http_handler& rhs) = default; const http_handler& operator=(http_sync_handler fn) { sync_handler = std::move(fn); @@ -205,6 +201,10 @@ struct HV_EXPORT HttpService { // @retval / => /var/www/html/index.html std::string GetStaticFilepath(const char* path); +#ifdef WITH_LUA + void Script(const char* path, const char* script_dir); +#endif + // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS void AllowCORS(); diff --git a/scripts/unittest.sh b/scripts/unittest.sh index f32edee13..2a795efa1 100755 --- a/scripts/unittest.sh +++ b/scripts/unittest.sh @@ -32,6 +32,9 @@ bin/socketpair_test # bin/objectpool_test bin/sizeof_test bin/http_router_test +if [ -x bin/http_lua_handler_test ]; then + bin/http_lua_handler_test +fi if [ -x bin/hdns_test ]; then bin/hdns_test fi diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index c92a3e1fb..9109d8420 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -90,6 +90,13 @@ target_include_directories(sendmail PRIVATE .. ../base ../protocol ../util) add_executable(http_router_test http_router_test.cpp) target_include_directories(http_router_test PRIVATE ../http/server) +if(WITH_LUA) +add_executable(http_lua_handler_test http_lua_handler_test.cpp) +target_include_directories(http_lua_handler_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../http ../http/server) +target_link_libraries(http_lua_handler_test ${HV_LIBRARIES}) +set(HTTP_LUA_UNITTEST_TARGETS http_lua_handler_test) +endif() + # ------event: async dns------ add_executable(hdns_test hdns_test.c) target_include_directories(hdns_test PRIVATE .. ../base ../ssl ../event) @@ -155,6 +162,7 @@ add_custom_target(unittest DEPENDS ftp sendmail http_router_test + ${HTTP_LUA_UNITTEST_TARGETS} hdns_test hdns_benchmark ${REDIS_UNITTEST_TARGETS} diff --git a/unittest/http_lua_handler_test.cpp b/unittest/http_lua_handler_test.cpp new file mode 100644 index 000000000..2f9919aee --- /dev/null +++ b/unittest/http_lua_handler_test.cpp @@ -0,0 +1,168 @@ +#include "HttpScriptHandler.h" + +#include +#include +#include + +#include "hbase.h" +#include "hfile.h" +#include "hpath.h" +#include "HttpService.h" +#include "HttpContext.h" + +static std::string write_script(const char* name, const char* content) { + std::string dir = "tmp/http_lua_handler_test"; + const char* slash = strrchr(name, '/'); + if (slash) { + dir = HPath::join(dir, std::string(name, slash - name)); + } + hv_mkdir_p(dir.c_str()); + std::string path = HPath::join("tmp/http_lua_handler_test", name); + HFile file; + int ret = file.open(path.c_str(), "wb"); + assert(ret == 0); + file.write(content, strlen(content)); + file.close(); + return path; +} + +static HttpContextPtr make_ctx(const char* method, const char* path) { + HttpContextPtr ctx = std::make_shared(); + ctx->request = std::make_shared(); + ctx->response = std::make_shared(); + ctx->request->method = http_method_enum(method); + ctx->request->path = path; + ctx->request->query_params["id"] = "42"; + ctx->request->headers["X-Test"] = "header-value"; + ctx->request->body = "request-body"; + return ctx; +} + +static void test_text_response() { + std::string script = write_script("text.lua", + "function handle(ctx)\n" + " ctx:status(201)\n" + " ctx:set_header('X-Lua', ctx:query('id'))\n" + " return ctx:text(ctx:method() .. ' ' .. ctx:path() .. ' ' .. ctx:header('X-Test'))\n" + "end\n"); + + hv::HttpScriptHandler handler(script.c_str()); + HttpContextPtr ctx = make_ctx("GET", "/hello"); + int status = handler(ctx); + assert(status == 201); + assert(ctx->response->status_code == 201); + assert(ctx->response->body == "GET /hello header-value"); + assert(ctx->response->GetHeader("X-Lua") == "42"); + assert(ctx->response->ContentType() == TEXT_PLAIN); +} + +static void test_json_response() { + std::string script = write_script("json.lua", + "function handle(ctx)\n" + " return ctx:json({ok=true, id=ctx:query('id')})\n" + "end\n"); + + hv::HttpScriptHandler handler(script.c_str()); + HttpContextPtr ctx = make_ctx("POST", "/json"); + int status = handler(ctx); + assert(status == 200); + assert(ctx->response->ContentType() == APPLICATION_JSON); + assert(ctx->response->body.find("\"ok\": true") != std::string::npos); + assert(ctx->response->body.find("\"id\": \"42\"") != std::string::npos); +} + +static void test_method_function_preferred() { + std::string script = write_script("method.lua", + "function get(ctx)\n" + " return ctx:text('get:' .. ctx:query('id'))\n" + "end\n" + "function handle(ctx)\n" + " return ctx:text('handle')\n" + "end\n"); + + hv::HttpScriptHandler handler(script.c_str()); + HttpContextPtr ctx = make_ctx("GET", "/method"); + int status = handler(ctx); + assert(status == 200); + assert(ctx->response->body == "get:42"); +} + +static void test_method_function_fallback_to_handle() { + std::string script = write_script("method_fallback.lua", + "function get(ctx)\n" + " return ctx:text('get')\n" + "end\n" + "function handle(ctx)\n" + " return ctx:text('fallback:' .. ctx:method())\n" + "end\n"); + + hv::HttpScriptHandler handler(script.c_str()); + HttpContextPtr ctx = make_ctx("POST", "/method"); + int status = handler(ctx); + assert(status == 200); + assert(ctx->response->body == "fallback:POST"); +} + +static void test_script_dir_mapping() { + write_script("api/user.lua", + "function handle(ctx)\n" + " return ctx:text('script:' .. ctx:query('id'))\n" + "end\n"); + + hv::HttpService service; + service.Script("/api/", "tmp/http_lua_handler_test/api"); + + http_handler* handler = NULL; + std::map params; + int ret = service.GetRoute("/api/user?id=42", HTTP_GET, &handler, params); + assert(ret == 0); + assert(handler != NULL); + assert(handler->ctx_handler != NULL); + + HttpContextPtr ctx = make_ctx("GET", "/api/user"); + ctx->service = &service; + ctx->request->query_params = params; + ctx->request->query_params["id"] = "42"; + int status = handler->ctx_handler(ctx); + assert(status == 200); + assert(ctx->response->body == "script:42"); +} + +static void test_script_dir_parent_path_forbidden() { + hv::HttpService service; + service.Script("/api/", "tmp/http_lua_handler_test/api"); + + http_handler* handler = NULL; + std::map params; + int ret = service.GetRoute("/api/foo/..bar", HTTP_GET, &handler, params); + assert(ret == 0); + assert(handler != NULL); + assert(handler->ctx_handler != NULL); + + HttpContextPtr ctx = make_ctx("GET", "/api/foo/..bar"); + ctx->service = &service; + int status = handler->ctx_handler(ctx); + assert(status == HTTP_STATUS_FORBIDDEN); +} + +static void test_unknown_script_suffix() { + std::string script = write_script("unknown.py", "def handle(ctx): pass\n"); + + hv::HttpScriptHandler handler(script.c_str()); + HttpContextPtr ctx = make_ctx("GET", "/unknown"); + int status = handler(ctx); + assert(status == HTTP_STATUS_NOT_IMPLEMENTED); + assert(ctx->response->status_code == HTTP_STATUS_NOT_IMPLEMENTED); +} + +int main() { + test_text_response(); + test_json_response(); + test_method_function_preferred(); + test_method_function_fallback_to_handle(); + test_script_dir_mapping(); + test_script_dir_parent_path_forbidden(); + test_unknown_script_suffix(); + printf("ALL http_lua_handler_test PASSED\n"); + return 0; +}