diff --git a/.gitignore b/.gitignore index 8fd2c36..3377b39 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,52 @@ /log /3rdparty/HP-Socket/lib +.fastweb_runtime +module/.install/1/fastweb.lua +module/.install/1/fastweb/auto_lock.lua +module/.install/1/fastweb/codec.lua +module/.install/1/fastweb/ini.lua +module/.install/1/fastweb/interceptor.lua +module/.install/1/fastweb/mutex.lua +module/.install/1/fastweb/process.lua +module/.install/1/fastweb/queue.lua +module/.install/1/fastweb/request.lua +module/.install/1/fastweb/response.lua +module/.install/1/fastweb/session.lua +module/.install/1/fastweb/subscribe.lua +module/.install/1/fastweb/timer.lua +module/.install/1/fastweb/websocket.lua +module/.install/9/base64.lua +module/.install/9/cache.lua +module/.install/9/httpclient.lua +module/.install/9/utils.lua +module/.install/9/aliyun/ai.lua +module/.install/9/aliyun/email.lua +module/.install/9/aliyun/oss.lua +module/.install/9/deepseek/ai.lua +module/.install/9/fwutils/config.lua +module/.install/9/fwutils/funs.lua +module/.install/9/fwutils/init.lua +module/.install/9/fwutils/request_config.lua +module/.install/9/fwutils/stopwatch.lua +module/.install/9/fwutils/token.lua +module/.install/9/fwutils/webapi.lua +module/.install/9/fwutils/develop/controll/menu.lua +module/.install/9/fwutils/develop/controll/role_permissions.lua +module/.install/9/fwutils/develop/controll/role.lua +module/.install/9/fwutils/develop/controll/template.lua +module/.install/9/fwutils/develop/function/menu.lua +module/.install/9/fwutils/develop/function/role_permissions.lua +module/.install/9/fwutils/develop/function/role.lua +module/.install/9/fwutils/develop/function/template.lua +module/.install/9/fwutils/fwutils/controll/upload.lua +module/.install/9/fwutils/fwutils/function/files.lua +module/.install/9/fwutils/fwutils/function/upload.lua +module/.install/9/submail/sms.lua +module/.install/9/tencent/board.lua +module/.install/9/tencent/cos.lua +module/.install/9/tencent/qywx.lua +module/.install/9/tencent/wxofficial.lua +module/.install/9/tencent/wxpay.lua +module/.install/9/tencent/board/generate_user_sig.lua +module/.install/9/unisound/unisound.lua diff --git a/.vscode/launch.json b/.vscode/launch.json index ac0ee90..e753a88 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,40 +1,45 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - { - "name": "(gdb) Launch", // 配置名称,可以根据需要修改 - "type": "cppdbg", // 调试器类型 - "request": "launch", // 请求类型:启动调试 - "program": "${workspaceFolder}/build/fastweb", // 指定要调试的可执行程序路径 - "args": [ - "start", - "/home/nianhua/www/config.ini" - ], // 程序运行时的命令行参数 - "stopAtEntry": false, // 是否在程序入口停止 - "cwd": "/home/nianhua/www", // 工作目录 - "environment": [], // 运行时的环境变量 - "externalConsole": false, // 是否使用外部终端 - "MIMode": "gdb", // 使用GDB作为调试器 - "miDebuggerPath": "/usr/bin/gdb", // GDB的路径 - "preLaunchTask": "build", // 调试前执行的任务 - "logging": { - "engineLogging": true // 启用引擎日志记录 - }, - "setupCommands": [ - { - "description": "Enable pretty-printing for gdb", - "text": "-enable-pretty-printing", - "ignoreFailures": true - }, - { - "description": "Set Disassembly Flavor to Intel", - "text": "-gdb-set disassembly-flavor intel", - "ignoreFailures": true - } - ] - } + { + "name": "fastweb: start", + "type": "gdb", + "request": "launch", + "target": "/root/fastweb/build/fastweb", + "arguments": "start config.ini", + "cwd": "/root/fastweb", + "gdbpath": "/usr/bin/gdb", + "valuesFormatting": "parseText", + "printCalls": false, + "stopAtEntry": false, + "autorun": [ + "set pagination off", + "set print thread-events off", + "set non-stop off", + "handle SIGPIPE nostop noprint pass", + "handle SIG33 nostop noprint pass", + "handle SIG34 nostop noprint pass", + "handle SIG60 nostop noprint pass", + "handle SIGUSR1 nostop noprint pass", + "handle SIGUSR2 nostop noprint pass", + "handle SIGABRT stop print nopass", + "break abort", + "break __assert_fail" + ] + }, + { + "name": "fastweb: start (CodeLLDB)", + "type": "lldb", + "request": "launch", + "program": "/usr/local/bin/fastweb", + "args": ["start", "config.ini"], + "cwd": "${workspaceFolder}", + "stopOnEntry": false, + "initCommands": [ + "settings set target.process.thread.step-out-avoid-nodebug false" + ], + "breakpointCommands": [] + } ] -} \ No newline at end of file + } + \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 2bdfadf..b87742e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,7 +104,7 @@ else() link_directories(/usr/local/lib) target_link_libraries(${FASTWEBCORE} hpsocket - ylib + ylib_d leveldb crypto /opt/lua54/lib/liblua.a @@ -145,7 +145,7 @@ else() target_link_libraries(${PROJECT_NAME} ${FASTWEBCORE} hpsocket - ylib + ylib_d pthread mysqlcppconn ssl diff --git a/config.ini b/config.ini index 0b65c6e..04d5b0b 100644 --- a/config.ini +++ b/config.ini @@ -25,6 +25,8 @@ domain=local.newobj.org,0.0.0.0 direct_url_mapping=1 ; 最大上传大小限制(MB),0为不限制 max_upload_size=10 +; 是否启用WebSocket +websocket_enable=1 [log] ; 开启或关闭 diff --git a/src/core/app.cpp b/src/core/app.cpp index df02302..2f6ab08 100644 --- a/src/core/app.cpp +++ b/src/core/app.cpp @@ -62,6 +62,7 @@ bool fastweb::app::start(const std::string& config_filepath) network::http::start_config config; network::http::website_config ws_config; config.max_upload_size = this->config->website.max_upload_size; + config.websocket_enable = this->config->website.websocket_enable; for_iter(iter, this->config->domain) { network::http::host_config host_config; diff --git a/src/core/config.cpp b/src/core/config.cpp index 64c98fc..e2048e6 100644 --- a/src/core/config.cpp +++ b/src/core/config.cpp @@ -80,7 +80,7 @@ void fastweb::config::cache() website.domain = strutils::split(m_ini.read("website", "domain"),','); website.direct_url_mapping = m_ini.read("website", "direct_url_mapping") == "1"; website.max_upload_size = ylib::stoll(m_ini.read("website", "max_upload_size")); - + website.websocket_enable = m_ini.read("website", "websocket_enable") == "1"; log.enable = m_ini.read("log", "enable") == "1"; log.dir = PATH_EX(m_ini.read("log", "dir")); log.name = m_ini.read("log", "name"); diff --git a/src/core/config.h b/src/core/config.h index 0b361e7..30643e4 100644 --- a/src/core/config.h +++ b/src/core/config.h @@ -32,6 +32,7 @@ namespace fastweb std::vector domain; bool direct_url_mapping = false; uint64 max_upload_size = 0; + bool websocket_enable = false; }; struct log { diff --git a/src/core/modulemanager.cpp b/src/core/modulemanager.cpp index 814086a..8be4cd4 100644 --- a/src/core/modulemanager.cpp +++ b/src/core/modulemanager.cpp @@ -29,6 +29,7 @@ If you have any questions, please contact us: 1585346868@qq.com Or visit our web #include "module/http/request.h" #include "module/http/response.h" +#include "module/http/websocket.h" #include "module/http/httpclient.h" #include "module/http/interceptor.h" #include "module/http/subscribe.h" @@ -194,6 +195,7 @@ void fastweb::module_manager::load_core(sol::state* lua) module::request::regist(lua); module::response::regist(lua); + module::websocket::regist(lua); module::interceptor::regist(lua); module::subscribe::regist(lua); module::httpclient::regist(lua); diff --git a/src/core/subscribemanager.cpp b/src/core/subscribemanager.cpp index 0e0ac29..b91e95d 100644 --- a/src/core/subscribemanager.cpp +++ b/src/core/subscribemanager.cpp @@ -21,6 +21,7 @@ If you have any questions, please contact us: 1585346868@qq.com Or visit our web #include "core/statemanager.h" #include "module/http/request.h" #include "module/http/response.h" +#include "module/http/websocket.h" #include "net/http_subscribe.h" fastweb::subscribe_manager::subscribe_manager(fastweb::app* app) :Interface(app) { @@ -32,14 +33,24 @@ fastweb::subscribe_manager::~subscribe_manager() void fastweb::subscribe_manager::start() { - app()->router->other([&](network::http::request* request, network::http::response* response) { - this->other(request, response); + app()->router->other([&](network::http::request* request, network::http::response* response,network::http::websocket_message* ws_message) { + this->other(request, response, ws_message); + }); + app()->router->on_close([&](uint64 connid,network::http::websocket_message* ws_message) { + // CLOSE 无 request/response;callback 内异常不可再抛出(上层无 catch,会 terminate) + try { + ws_message->connid = connid; + this->callback(nullptr, nullptr, ws_message, "", ws_message->filepath); + } + catch (const std::exception& e) { + LOG_ERROR(std::string("[subscribe][ws_close]: ") + e.what()); + } }); } bool fastweb::subscribe_manager::add(const std::string& pattern, const std::string& filepath) { - app()->router->subscribe()->add(pattern, filepath, [&](network::http::request* request, network::http::response* response, const std::string& pattern, const std::string& filepath)->bool { - return this->callback(request, response, pattern, filepath); + app()->router->subscribe()->add(pattern, filepath, [&](network::http::request* request, network::http::response* response,network::http::websocket_message* ws_message, const std::string& pattern, const std::string& filepath)->bool { + return this->callback(request, response, ws_message, pattern, filepath); }); return true; } @@ -57,7 +68,7 @@ void fastweb::subscribe_manager::clear() app()->router->subscribe()->clear(); } -bool fastweb::subscribe_manager::callback(network::http::request* request, network::http::response* response, const std::string& pattern, const std::string& filepath) +bool fastweb::subscribe_manager::callback(network::http::request* request, network::http::response* response,network::http::websocket_message* ws_message, const std::string& pattern, const std::string& filepath) { bool ok_continue = false; auto lua = app()->state->get(); @@ -68,12 +79,13 @@ bool fastweb::subscribe_manager::callback(network::http::request* request, netwo if (!script.valid()) { sol::error err = script; throw ylib::exception(err.what()); - } + } module::request m_request(request); module::response m_response(response); + module::websocket m_websocket(ws_message); (*lua->state)["fw_response"] = m_response; (*lua->state)["fw_request"] = m_request; - + (*lua->state)["fw_websocket"] = m_websocket; sol::protected_function_result result = script(); if (!result.valid()) { sol::error err = result; @@ -84,19 +96,25 @@ bool fastweb::subscribe_manager::callback(network::http::request* request, netwo { exception_string = e.what(); if (app()->config->website.debug) - LOG_ERROR("[subscribe][" + request->filepath() + "]: " + e.what()); + { + std::string where = filepath; + if (where.empty() && request != nullptr) + where = request->filepath(); + LOG_ERROR("[subscribe][" + where + "]: " + e.what()); + } } // 释放lua资源 lua->state->collect_garbage(); app()->state->push(lua); - if (exception_string.empty() == false) + // CLOSE(request/response 为空)只记日志,不再上抛,避免线程里未捕获异常 abort + if (exception_string.empty() == false && request != nullptr) throw ylib::exception(exception_string); return ok_continue; } -void fastweb::subscribe_manager::other(network::http::request* request, network::http::response* response) +void fastweb::subscribe_manager::other(network::http::request* request, network::http::response* response,network::http::websocket_message* ws_message) { std::string filepath = request->filepath(); if (filepath.empty()) @@ -129,7 +147,7 @@ void fastweb::subscribe_manager::other(network::http::request* request, network: } else if(ext == "lua" && app()->config->website.direct_url_mapping) { - callback(request, response, filepath, filepath); + callback(request, response, ws_message, filepath, filepath); return; } response->send_file(absolute_path); diff --git a/src/core/subscribemanager.h b/src/core/subscribemanager.h index 3a34876..d6ef1fa 100644 --- a/src/core/subscribemanager.h +++ b/src/core/subscribemanager.h @@ -31,13 +31,13 @@ namespace fastweb /// /// /// - bool callback(network::http::request* request, network::http::response* response, const std::string& pattern, const std::string& filepath); + bool callback(network::http::request* request, network::http::response* response,network::http::websocket_message* ws_message, const std::string& pattern, const std::string& filepath); /// /// 其它 /// /// /// - void other(network::http::request* request, network::http::response* response); + void other(network::http::request* request, network::http::response* response,network::http::websocket_message* ws_message); private: /// /// 未找到页面 diff --git a/src/module/http/request.cpp b/src/module/http/request.cpp index b9c50bb..e3d8003 100644 --- a/src/module/http/request.cpp +++ b/src/module/http/request.cpp @@ -33,6 +33,8 @@ module::request::~request() } sol::table module::request::body_param(sol::this_state s) { + if (m_request == nullptr) + throw ylib::exception("request is null"); sol::state_view lua(s); sol::table result_table = lua.create_table(); auto &map = m_request->body_param(); @@ -42,6 +44,8 @@ sol::table module::request::body_param(sol::this_state s) } sol::table module::request::url_param(sol::this_state s) { + if (m_request == nullptr) + throw ylib::exception("request is null"); sol::state_view lua(s); sol::table result_table = lua.create_table(); auto& map = m_request->url_param(); @@ -51,10 +55,14 @@ sol::table module::request::url_param(sol::this_state s) } std::string module::request::body() { + if (m_request == nullptr) + throw ylib::exception("request is null"); return m_request->body(); } sol::table module::request::multipart(sol::this_state s) { + if (m_request == nullptr) + throw ylib::exception("request is null"); auto multipart = m_request->multipart(); sol::state_view lua(s); sol::table result_table = lua.create_table(); @@ -80,12 +88,16 @@ sol::table module::request::multipart(sol::this_state s) } std::string module::request::multipart_content(int id) { + if (m_request == nullptr) + throw ylib::exception("request is null"); multipart_content_check(id); auto mult_info = m_request->multipart()->at(id - 1); return std::string(m_request->body().data() + mult_info.offset, mult_info.length); } bool module::request::multipart_content_save(int id, const std::string& filepath) { + if (m_request == nullptr) + throw ylib::exception("request is null"); ylib::file_io file; if (file.open(filepath) == false) { @@ -126,14 +138,20 @@ bool module::request::multipart_content_save(int id, const std::string& filepath //} void module::request::set(const std::string& name, const std::string& value) { + if (m_request == nullptr) + throw ylib::exception("request is null"); m_request->reqpack()->extra()[name] = value; } std::string module::request::get(const std::string& name) { + if (m_request == nullptr) + throw ylib::exception("request is null"); return m_request->reqpack()->extra()[name].to(); } sol::table module::request::gets(sol::this_state s) { + if (m_request == nullptr) + throw ylib::exception("request is null"); sol::state_view lua(s); sol::table result_table = lua.create_table(); @@ -146,10 +164,14 @@ sol::table module::request::gets(sol::this_state s) } network::http::website* module::request::website() { + if (m_request == nullptr) + throw ylib::exception("request is null"); return m_request->website(); } bool module::request::save_body(const std::string& filepath) { + if (m_request == nullptr) + throw ylib::exception("request is null"); return ylib::file::write(filepath, m_request->body()); } void module::request::regist(sol::state* lua) @@ -179,6 +201,8 @@ void module::request::regist(sol::state* lua) void module::request::multipart_content_check(int id) { + if (m_request == nullptr) + throw ylib::exception("request is null"); if (m_request->multipart()->size() < id) { throw ylib::exception("The maximum ID is " + std::to_string(m_request->multipart()->size()) + ", and the incoming ID is " + std::to_string(id)); @@ -191,12 +215,16 @@ void module::request::multipart_content_check(int id) } std::string module::request::header(const std::string& name) { + if (m_request == nullptr) + throw ylib::exception("request is null"); std::string value; m_request->header(name, value); return value; } sol::table module::request::headers(sol::this_state s) { + if (m_request == nullptr) + throw ylib::exception("request is null"); sol::state_view lua(s); sol::table result_table = lua.create_table(); @@ -207,18 +235,26 @@ sol::table module::request::headers(sol::this_state s) } std::string module::request::method() { + if (m_request == nullptr) + throw ylib::exception("request is null"); return m_request->method(); } std::string module::request::filepath() { + if (m_request == nullptr) + throw ylib::exception("request is null"); return m_request->filepath(); } std::string module::request::host() { + if (m_request == nullptr) + throw ylib::exception("request is null"); return m_request->host(); } sol::object module::request::param(const std::string& name, bool throw_,sol::this_state s) { + if (m_request == nullptr) + throw ylib::exception("request is null"); std::string value; bool result = request_param(name, value); if (result == false){ @@ -232,14 +268,20 @@ sol::object module::request::param(const std::string& name, bool throw_,sol::thi std::string module::request::remote_ipaddress() { + if (m_request == nullptr) + throw ylib::exception("request is null"); return m_request->remote().address; } ushort module::request::remote_port() { + if (m_request == nullptr) + throw ylib::exception("request is null"); return m_request->remote().port; } bool module::request::request_param(const std::string& name, std::string& value) { + if (m_request == nullptr) + throw ylib::exception("request is null"); if (m_request->get_url_param(name, value) == false) return m_request->get_body_param(name, value); return true; diff --git a/src/module/http/request.h b/src/module/http/request.h index 3b29c85..b52cf75 100644 --- a/src/module/http/request.h +++ b/src/module/http/request.h @@ -115,9 +115,6 @@ namespace module /// /// bool save_body(const std::string& filepath); - - - /// /// 推送到自动释放队列 /// diff --git a/src/module/http/response.cpp b/src/module/http/response.cpp index ef7b146..4169ef8 100644 --- a/src/module/http/response.cpp +++ b/src/module/http/response.cpp @@ -61,11 +61,23 @@ void module::response::sets(const sol::table& lua_table) m_sets = parse_lua_table(lua_table, parse_lua_table); } - +void module::response::response_done() +{ + if (m_response == nullptr) + throw ylib::exception("response is null"); + m_response->response_done(); +} +bool module::response::is_response_done() +{ + if (m_response == nullptr) + throw ylib::exception("response is null"); + return m_response->is_response_done(); +} void module::response::regist(sol::state* lua) { // 绑定 Request 类到 Lua lua->new_usertype("fw_response", + "send_header", &module::response::send_header, "send_data", &module::response::send_data, "send", &module::response::send, "sendex", &module::response::sendex, @@ -74,23 +86,46 @@ void module::response::regist(sol::state* lua) "redirect", &module::response::redirect, "forward", &module::response::forward, "set", &module::response::set, - "sets", &module::response::sets + "sets", &module::response::sets, + "response_done", &module::response::response_done, + "is_response_done", &module::response::is_response_done, + "send_ws", &module::response::send_ws ); } +bool module::response::send_ws(const std::string_view& data, int opcode) +{ + if (m_response == nullptr) + throw ylib::exception("response is null"); + return m_response->send_ws(data.data(), data.size(), opcode); +} +bool module::response::send_header(ushort stateNum, const std::string& stateDesc) +{ + if (m_response == nullptr) + throw ylib::exception("response is null"); + return m_response->send_header(stateNum, stateDesc); +} bool module::response::send_data(const char* buf, size_t buf_len, ushort stateNum, const std::string& stateDesc) { + if (m_response == nullptr) + throw ylib::exception("response is null"); return m_response->send(buf, buf_len, stateNum, stateDesc); } bool module::response::send(const std::string& value) { + if (m_response == nullptr) + throw ylib::exception("response is null"); return m_response->send(value); } bool module::response::sendex(const std::string& value, ushort stateNum, const std::string& stateDesc) { + if (m_response == nullptr) + throw ylib::exception("response is null"); return m_response->send(value, stateNum, stateDesc); } bool module::response::send_file(const std::string& filepath, int32 downbaud, ushort stateNum, const std::string& stateDesc) { + if (m_response == nullptr) + throw ylib::exception("response is null"); std::string end_filepath = system::temp_path() + "/" + codec::md5(filepath) + ".tmp"; if (m_sets.size() != 0 && ylib::file::size(filepath) < 1024 * 1024) @@ -106,13 +141,19 @@ bool module::response::send_file(const std::string& filepath, int32 downbaud, us } bool module::response::redirect(const std::string& filepath, bool MovedPermanently) { + if (m_response == nullptr) + throw ylib::exception("response is null"); return m_response->redirect(filepath, MovedPermanently); } bool module::response::forward(const std::string& filepath) { + if (m_response == nullptr) + throw ylib::exception("response is null"); return m_response->forward(filepath); } void module::response::header(const std::string& name, const std::string& value) { + if (m_response == nullptr) + throw ylib::exception("response is null"); m_response->headers()->emplace(name, value); } \ No newline at end of file diff --git a/src/module/http/response.h b/src/module/http/response.h index 1a9d903..e0b1b0e 100644 --- a/src/module/http/response.h +++ b/src/module/http/response.h @@ -11,6 +11,8 @@ namespace module response(network::http::response* response); ~response(); + bool send_ws(const std::string_view& data, int opcode = 0x1); + bool send_header(ushort stateNum, const std::string& stateDesc); bool send_data(const char* buf, size_t buf_len, ushort stateNum, const std::string& stateDesc); bool send(const std::string& value); bool sendex(const std::string& value, ushort stateNum, const std::string& stateDesc); @@ -21,6 +23,11 @@ namespace module void set(const std::string& name,const std::string& value); void sets(const sol::table& lua_table); + + + void response_done(); + bool is_response_done(); + static void regist(sol::state* lua); private: network::http::response* m_response = nullptr; diff --git a/src/module/http/websocket.cpp b/src/module/http/websocket.cpp new file mode 100644 index 0000000..ede86a5 --- /dev/null +++ b/src/module/http/websocket.cpp @@ -0,0 +1,96 @@ +/*Software License + +Copyright(C) 2024[liuyingjie] +License Terms +Usage Rights + +Any individual or entity is free to use, copy, and distribute the binary form of this software without modification to the source code, without the need to disclose the source code. +If the source code is modified, the modifications must be open - sourced under the same license.This means that the modifications must be disclosed and accompanied by a copy of this license. +Future Versions Updates +From this version onwards, all future releases will be governed by the terms of the latest version of the license.This license will automatically be nullified and replaced by the new version. +Users must comply with the terms of the new license issued in future releases. +Liability and Disclaimer +This software is provided “as is”, without any express or implied warranties, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non - infringement.In no event shall the author or copyright holder be liable for any claims, damages, or other liabilities, 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. +Contact Information +If you have any questions, please contact us: 1585346868@qq.com Or visit our website fwlua.com. +*/ + +#include "websocket.h" + +module::websocket::websocket(network::http::websocket_message* ws_message) :m_ws_message(ws_message) +{ +} +module::websocket::~websocket() +{ +} +bool module::websocket::valid() +{ + if (m_ws_message == nullptr) + return false; + return true; +} +int module::websocket::type() +{ + if (m_ws_message == nullptr) + throw ylib::exception("websocket message is null"); + return (int)m_ws_message->type; +} +bool module::websocket::is_final() +{ + if (m_ws_message == nullptr) + throw ylib::exception("websocket message is null"); + return m_ws_message->header._final; +} +int module::websocket::opcode() +{ + if (m_ws_message == nullptr) + throw ylib::exception("websocket message is null"); + return m_ws_message->header.opcode; +} +uint64 module::websocket::length() +{ + if (m_ws_message == nullptr) + throw ylib::exception("websocket message is null"); + return m_ws_message->header.length; +} +sol::table module::websocket::mask(sol::this_state s) +{ + if (m_ws_message == nullptr) + throw ylib::exception("websocket message is null"); + sol::state_view lua(s); + sol::table result = lua.create_table(4, 0); + for (int i = 0; i < 4; i++) + result[i + 1] = (int)m_ws_message->header.mask[i]; + return result; +} +std::string module::websocket::sec_websocket_key() +{ + if (m_ws_message == nullptr) + throw ylib::exception("websocket message is null"); + return m_ws_message->sec_websocket_key; +} +uint64 module::websocket::connid() +{ + if (m_ws_message == nullptr) + throw ylib::exception("websocket message is null"); + return m_ws_message->connid; +} +void module::websocket::regist(sol::state* lua) +{ + lua->new_usertype("fw_websocket", + "type", &module::websocket::type, + "final", &module::websocket::is_final, + "opcode", &module::websocket::opcode, + "length", &module::websocket::length, + "mask", &module::websocket::mask, + "valid", &module::websocket::valid, + "sec_websocket_key", &module::websocket::sec_websocket_key, + "connid", &module::websocket::connid + ); + + // ws_type 枚举,便于脚本判断 + (*lua)["FW_WS_UPGRADE"] = (int)network::http::HTTP_SERVER_WEBSOCKET_TYPE_UPGRADE; + (*lua)["FW_WS_MESSAGE_HEADER"] = (int)network::http::HTTP_SERVER_WEBSOCKET_TYPE_MESSAGE_HEADER; + (*lua)["FW_WS_MESSAGE_BODY"] = (int)network::http::HTTP_SERVER_WEBSOCKET_TYPE_MESSAGE_BODY; + (*lua)["FW_WS_CLOSE"] = (int)network::http::HTTP_SERVER_WEBSOCKET_TYPE_CLOSE; +} diff --git a/src/module/http/websocket.h b/src/module/http/websocket.h new file mode 100644 index 0000000..ae6939b --- /dev/null +++ b/src/module/http/websocket.h @@ -0,0 +1,58 @@ +#pragma once +#include "net/http_request.h" +#include "net/http_response.h" + +#include "sol/sol.hpp" +#include "core/define.h" +namespace module +{ + class websocket + { + public: + websocket(network::http::websocket_message* ws_message); + ~websocket(); + + + + /// + /// 是否有效 + /// + bool valid(); + + /// + /// 消息类型 (ws_type) + /// + int type(); + /// + /// 是否结束帧 + /// + bool is_final(); + /// + /// 帧操作码 + /// + int opcode(); + /// + /// 载荷长度 + /// + uint64 length(); + /// + /// 掩码 (4字节) + /// + sol::table mask(sol::this_state s); + + /// + /// Sec-WebSocket-Key + /// + std::string sec_websocket_key(); + + /// + /// 取连接ID + /// + uint64 connid(); + + static void regist(sol::state* lua); + private: + network::http::websocket_message* m_ws_message = nullptr; + }; + +} diff --git a/www/api/sha1.lua b/www/api/sha1.lua new file mode 100644 index 0000000..ae00202 --- /dev/null +++ b/www/api/sha1.lua @@ -0,0 +1,14 @@ +-- GET /api/sha1.lua 测试 WebSocket Accept 的 SHA1/Base64 算法 +-- RFC6455 样例: +-- Key = dGhlIHNhbXBsZSBub25jZQ== +-- Accept = s3pPLMBiTxaQ9kYGzzhZRbK+xOo= +-- +-- 可选参数:?key=你的Sec-WebSocket-Key + +local request = require("fastweb.request") +local response = require("fastweb.response") +local codec = require("fastweb.codec") +local base64 = require("base64") +local utils = require("utils") +local cjson = require("cjson") +print("headers:",cjson.encode(request.headers())) \ No newline at end of file diff --git a/www/api/ws.lua b/www/api/ws.lua new file mode 100644 index 0000000..9e760d7 --- /dev/null +++ b/www/api/ws.lua @@ -0,0 +1,77 @@ +local request = require("fastweb.request") +local response = require("fastweb.response") +local ws = require("fastweb.websocket") +local codec = require("fastweb.codec") +local base64 = require("base64") +local utils = require("utils") + + +if not ws.valid() then + response.response_done() + return +end + +if ws.type() == ws.UPGRADE then + print("[WS_UPGRADE] CONNID:" .. ws.connid()) + local accept_key = base64.encode(utils.hex_to_bytes(codec.sha1(ws.sec_websocket_key() .. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))) + response.header("Sec-WebSocket-Accept", accept_key) + response.header("Connection", "Upgrade") + response.header("Upgrade", "websocket") + response.send_header(101, "Switching Protocols") + +elseif ws.type() == ws.MESSAGE_HEADER then + local opcode = ws.opcode() + + local log = "[WS_HEADER] CONNID:" .. ws.connid() + if opcode == ws.OPCODE_TEXT then + log = log .. " [TEXT]" + elseif opcode == ws.OPCODE_BINARY then + log = log .. " [BINARY]" + elseif opcode == ws.OPCODE_CLOSE then + log = log .. " [CLOSE]" + elseif opcode == ws.OPCODE_PING then + log = log .. " [PING]" + elseif opcode == ws.OPCODE_PONG then + log = log .. " [PONG]" + else + log = log .. " [OPCODE:" .. opcode .. "]" + end + log = log .. " [FINAL:" .. tostring(ws.final()) .. "] [LENGTH:" .. ws.length() .. "]" + print(log) + +elseif ws.type() == ws.MESSAGE_BODY then + local connid = ws.connid() + local opcode = ws.opcode() + local body = request.body() + local log = "[WS_BODY] CONNID:" .. connid + .. " [OPCODE:" .. tostring(opcode) .. "]" + .. " [LENGTH:" .. tostring(#body) .. "]" + print(log) + + if opcode == ws.OPCODE_CLOSE then + -- 客户端关闭:回 Close 帧(可带回原载荷),完成握手 + print("[WS_BODY] reply CLOSE") + response.send_ws(body, ws.OPCODE_CLOSE) + elseif opcode == ws.OPCODE_PING then + -- Ping 必须回 Pong,载荷原样带回 + print("[WS_BODY] reply PONG") + response.send_ws(body, ws.OPCODE_PONG) + elseif opcode == ws.OPCODE_PONG then + -- Pong 无需回复 + print("[WS_BODY] PONG ignored") + elseif opcode == ws.OPCODE_TEXT or opcode == ws.OPCODE_BINARY then + -- 文本/二进制:原样回显 + response.send_ws(body, opcode) + else + print("[WS_BODY] unhandled opcode:" .. tostring(opcode)) + end + +elseif ws.type() == ws.CLOSE then + -- TCP 已断开,只做清理,不要再发帧 + print("[WS_CLOSE] CONNID:" .. ws.connid()) + return +else + print("UNKNOWN") +end + +response.response_done() diff --git a/www/index.html b/www/index.html index 0cbf303..0520ad1 100644 --- a/www/index.html +++ b/www/index.html @@ -1,113 +1,345 @@ - + - - - 用户登录 - - - + body { + font-family: "IBM Plex Sans", "Segoe UI", sans-serif; + background: + radial-gradient(ellipse 80% 50% at 20% -10%, #1a3050 0%, transparent 55%), + radial-gradient(ellipse 60% 40% at 90% 100%, #152030 0%, transparent 50%), + var(--bg); + color: var(--text); + min-height: 100vh; + padding: 2rem 1.25rem 3rem; + line-height: 1.5; + } + + main { + max-width: 720px; + margin: 0 auto; + } + + h1 { + font-family: "IBM Plex Mono", "Cascadia Code", monospace; + font-size: 1.35rem; + font-weight: 600; + letter-spacing: -0.02em; + margin-bottom: 0.35rem; + } + + .sub { + color: var(--muted); + font-size: 0.9rem; + margin-bottom: 1.75rem; + } + + .sub code { + font-family: "IBM Plex Mono", monospace; + font-size: 0.85em; + color: var(--accent); + } + + .row { + display: flex; + flex-wrap: wrap; + gap: 0.6rem; + align-items: center; + margin-bottom: 1rem; + } + + .status { + display: inline-flex; + align-items: center; + gap: 0.45rem; + font-size: 0.85rem; + color: var(--muted); + margin-left: auto; + } + + .dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--muted); + } + + .dot.open { background: var(--ok); box-shadow: 0 0 8px var(--ok); } + .dot.connecting { background: var(--warn); } + .dot.closed, .dot.error { background: var(--err); } + + button, input { + font: inherit; + } + + button { + background: var(--accent); + color: #0a1220; + border: none; + padding: 0.55rem 1.1rem; + font-weight: 600; + font-size: 0.875rem; + cursor: pointer; + transition: background 0.15s; + } + + button:hover:not(:disabled) { background: var(--accent-hover); } + button:disabled { + opacity: 0.4; + cursor: not-allowed; + } + + button.ghost { + background: transparent; + color: var(--text); + border: 1px solid var(--border); + } + + button.ghost:hover:not(:disabled) { + border-color: var(--muted); + background: var(--surface); + } + + .url { + width: 100%; + background: var(--surface); + border: 1px solid var(--border); + color: var(--text); + padding: 0.65rem 0.85rem; + font-family: "IBM Plex Mono", monospace; + font-size: 0.85rem; + margin-bottom: 1rem; + } + + .url:focus { + outline: none; + border-color: var(--accent); + } + + .send-row { + display: flex; + gap: 0.6rem; + margin-bottom: 1rem; + } + + .send-row input { + flex: 1; + background: var(--surface); + border: 1px solid var(--border); + color: var(--text); + padding: 0.65rem 0.85rem; + font-size: 0.9rem; + } + + .send-row input:focus { + outline: none; + border-color: var(--accent); + } + + .log { + background: var(--surface); + border: 1px solid var(--border); + height: min(52vh, 420px); + overflow-y: auto; + padding: 0.75rem; + font-family: "IBM Plex Mono", monospace; + font-size: 0.8rem; + } + + .log .line { + padding: 0.2rem 0; + border-bottom: 1px solid transparent; + word-break: break-all; + white-space: pre-wrap; + } + + .log .line .t { color: var(--muted); margin-right: 0.5rem; } + .log .line.sys { color: var(--muted); } + .log .line.out { color: var(--accent); } + .log .line.in { color: var(--ok); } + .log .line.err { color: var(--err); } + -