diff --git a/Base/CMakeLists.txt b/Base/CMakeLists.txt index 2232222..4582959 100644 --- a/Base/CMakeLists.txt +++ b/Base/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(Base + Database.h Database.cpp DataStructure.h DataStructure.cpp HttpSession.h HttpSession.cpp ) @@ -12,4 +13,5 @@ target_link_libraries(Base PRIVATE OpenSSL::Crypto PUBLIC Kylin::Router PUBLIC Kylin::Core + PRIVATE sqlite3 ) \ No newline at end of file diff --git a/Base/DataStructure.h b/Base/DataStructure.h index a039483..c3fc80a 100644 --- a/Base/DataStructure.h +++ b/Base/DataStructure.h @@ -13,6 +13,15 @@ struct VisitorStats { int64_t lastViewTime = 0; }; +struct VisitRecord { + int id; + std::string url; + std::string visitorUuid; + std::string lastUserAgent; + int lastViewTime; + int pageViewCount; +}; + struct SiteStats { int totalViews = 0; int totalVisitors = 0; diff --git a/Server/Database.cpp b/Base/Database.cpp similarity index 82% rename from Server/Database.cpp rename to Base/Database.cpp index 97dc189..16812d3 100644 --- a/Server/Database.cpp +++ b/Base/Database.cpp @@ -5,7 +5,7 @@ namespace Older { bool Database::open(const std::string &path) { - if (sqlite3_open(path.c_str(), &m_sqlite)) { + if (sqlite3_open(path.c_str(), &m_sqlite) != SQLITE_OK) { LOG(error) << "Can't open database: " << sqlite3_errmsg(m_sqlite); return false; } @@ -65,7 +65,7 @@ std::list Database::mostViewedUrls(int n) { ORDER BY total_page_views DESC LIMIT ?; )"; - + if (sqlite3_prepare_v2(m_sqlite, query, -1, &statement, nullptr) == SQLITE_OK) { sqlite3_bind_int(statement, 1, n); @@ -228,6 +228,59 @@ Account Database::user(int64_t id) const { return ret; } +std::list Database::visitRecords() { + constexpr char *sql = "SELECT * FROM visit_analysis;"; + sqlite3_stmt *statement = nullptr; + std::list ret; + if (sqlite3_prepare_v2(m_sqlite, sql, -1, &statement, nullptr) != SQLITE_OK) { + LOG(error) << "sqlite3_prepare_v2() failed: " << sqlite3_errmsg(m_sqlite); + return ret; + } + + while (sqlite3_step(statement) == SQLITE_ROW) { + VisitRecord record; + record.id = sqlite3_column_int(statement, 0); + record.url = reinterpret_cast(sqlite3_column_text(statement, 1)); + record.visitorUuid = reinterpret_cast(sqlite3_column_text(statement, 2)); + record.lastUserAgent = reinterpret_cast(sqlite3_column_text(statement, 3)); + record.lastViewTime = sqlite3_column_int(statement, 4); + record.pageViewCount = sqlite3_column_int(statement, 5); + ret.push_back(record); + } + sqlite3_finalize(statement); + return ret; +} + +bool Database::removeVisitRecord(int id) { + constexpr char *sql = "DELETE FROM visit_analysis WHERE id = ?;"; + sqlite3_stmt *statement = nullptr; + if (sqlite3_prepare_v2(m_sqlite, sql, -1, &statement, nullptr) != SQLITE_OK) { + LOG(error) << "sqlite3_prepare_v2() failed: " << sqlite3_errmsg(m_sqlite); + return false; + } + + if (sqlite3_bind_int(statement, 1, id) != SQLITE_OK) { + LOG(error) << "sqlite3_bind_int() failed: " << sqlite3_errmsg(m_sqlite); + sqlite3_finalize(statement); + return false; + } + bool success = false; + + if (sqlite3_step(statement) == SQLITE_DONE) { + if (sqlite3_changes(m_sqlite) > 0) { + success = true; + } else { + LOG(warning) << "cannot find item " << id; + } + } else { + LOG(error) << "sqlite3_step() failed: " << sqlite3_errmsg(m_sqlite); + } + + // 清理资源 + sqlite3_finalize(statement); + return success; +} + void Database::initialize() { createVisitAnalysisTable(); createUsersTable(); diff --git a/Server/Database.h b/Base/Database.h similarity index 77% rename from Server/Database.h rename to Base/Database.h index 34700c3..cee6e29 100644 --- a/Server/Database.h +++ b/Base/Database.h @@ -1,7 +1,7 @@ #ifndef __DATABASE_H__ #define __DATABASE_H__ -#include "Base/DataStructure.h" +#include "DataStructure.h" #include #include @@ -18,9 +18,11 @@ public: std::list mostViewedUrls(int n); std::list latestViewedUrls(int n); SiteStats siteStats(); + std::list visitRecords(); + bool removeVisitRecord(int id); void createUser(const Account &account); - Account user(const std::string &identifier)const; - Account user(int64_t id)const; + Account user(const std::string &identifier) const; + Account user(int64_t id) const; protected: void createVisitAnalysisTable(); @@ -29,6 +31,7 @@ protected: private: sqlite3 *m_sqlite = nullptr; -};} +}; +} // namespace Older #endif // __DATABASE_H__ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 06551b6..d43e965 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,14 +5,21 @@ project(Older VERSION 0.1 LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Boost REQUIRED COMPONENTS json) +find_package(Boost REQUIRED COMPONENTS json program_options) find_package(OpenSSL REQUIRED) +execute_process( + COMMAND git rev-parse --short HEAD + OUTPUT_VARIABLE GIT_COMMIT_ID + OUTPUT_STRIP_TRAILING_WHITESPACE +) + add_subdirectory(3rdparty) add_subdirectory(Base) add_subdirectory(Server) add_subdirectory(WebRTC) add_subdirectory(WeChat) +add_subdirectory(Tools) add_subdirectory(UnitTest) include(FetchContent) diff --git a/Readme.md b/Readme.md index 0a4fc74..40fae36 100644 --- a/Readme.md +++ b/Readme.md @@ -30,6 +30,28 @@ 5. 将 url 的 last_view_time进行排序,统计出最新的 n 个记录。 6. 将所有记录的不同 visitor_uuid 相加作为网站总访客数,将所有page_view_count相加作为网站总访问次数。 +### 问题 + +sqlite3 如何将所有记录的 url 字段(TEXT类型) 中的 工作笔记 替换为 漫步闲谈?例如 /工作笔记/abc 替换为 /漫步闲谈/abc + +```sqlite +sqlite3 ./database.sqlite + +DELETE FROM visit_analysis WHERE rowid IN ( + SELECT t1.rowid + FROM visit_analysis t1 + JOIN visit_analysis t2 + ON t1.visitor_uuid = t2.visitor_uuid + AND REPLACE(t1.url, '工作笔记', '漫步闲谈') = t2.url + AND t1.rowid != t2.rowid + WHERE t1.url LIKE '%工作笔记%' +); + +UPDATE visit_analysis SET url = REPLACE(url, '工作笔记', '漫步闲谈') WHERE url LIKE '%工作笔记%'; + +.quit +``` + ## 注册/登录 ```json diff --git a/Server/Application.cpp b/Server/Application.cpp index 26ce2dc..ee9d628 100644 --- a/Server/Application.cpp +++ b/Server/Application.cpp @@ -1,10 +1,10 @@ #include "Application.h" +#include "Base/Database.h" #include "Base/HttpSession.h" #include "Base/Messages.h" #include "Core/IoContext.h" #include "Core/MessageManager.h" #include "Core/Singleton.h" -#include "Database.h" #include "Nng/Asio.h" #include "Nng/Message.h" #include "Nng/Socket.h" diff --git a/Server/CMakeLists.txt b/Server/CMakeLists.txt index 8ad6c10..dee755d 100644 --- a/Server/CMakeLists.txt +++ b/Server/CMakeLists.txt @@ -1,6 +1,7 @@ +configure_file(Configuration.h.in Configuration.h) + add_executable(Older main.cpp Application.h Application.cpp - Database.h Database.cpp ResponseUtility.h ResponseUtility.cpp ServiceLogic.h ServiceLogic.inl ServiceLogic.cpp SessionStore.h SessionStore.cpp @@ -9,6 +10,7 @@ add_executable(Older main.cpp target_include_directories(Older PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} + PRIVATE ${CMAKE_CURRENT_BINARY_DIR} ) target_link_libraries(Older @@ -17,5 +19,5 @@ target_link_libraries(Older PRIVATE Kylin::Http PRIVATE Kylin::Nng PRIVATE Boost::json - PRIVATE sqlite3 + PRIVATE Boost::program_options ) \ No newline at end of file diff --git a/Server/Configuration.h.in b/Server/Configuration.h.in new file mode 100644 index 0000000..4dbb2a4 --- /dev/null +++ b/Server/Configuration.h.in @@ -0,0 +1,2 @@ +#define GIT_COMMIT_ID "@GIT_COMMIT_ID@" +#define APP_VERSION "@PROJECT_VERSION@" diff --git a/Server/ServiceLogic.cpp b/Server/ServiceLogic.cpp index 6da1ff1..2dde838 100644 --- a/Server/ServiceLogic.cpp +++ b/Server/ServiceLogic.cpp @@ -1,7 +1,7 @@ #include "ServiceLogic.h" +#include "Base/Database.h" #include "Base/HttpSession.h" #include "Core/Singleton.h" -#include "Database.h" #include "SessionStore.h" #include "Settings.h" #include diff --git a/Server/main.cpp b/Server/main.cpp index 6d9fa47..7810e5a 100644 --- a/Server/main.cpp +++ b/Server/main.cpp @@ -1,19 +1,57 @@ #include "Application.h" +#include "Configuration.h" #include "Core/Logger.h" #include "Core/Singleton.h" #include +#include +#include +#include int main(int argc, char const *argv[]) { using namespace Core; using namespace Older; + + boost::program_options::options_description description("Allowed options"); + // clang-format off + description.add_options() + ("help,h", "produce help message.") + ("version,v", "print app version.") + ("exit,e", "signal program to exit.") + ("prefix", boost::program_options::value(),"set prefix path (default: ${pwd})"); + // clang-format on + boost::program_options::variables_map values; + boost::log::initialize("logs/Older"); - auto application = Singleton::construct(); - boost::asio::signal_set signals(application->ioContext(), SIGINT, SIGTERM, SIGHUP); - signals.async_wait([&application](boost::system::error_code const &, int signal) { - LOG(info) << "capture " << (signal == SIGINT ? "SIGINT" : "SIGTERM") << ",stop!"; - application->exit(5); - }); + try { + boost::program_options::store(boost::program_options::parse_command_line(argc, argv, description), values); + boost::program_options::notify(values); - return application->exec(); + if (values.count("help")) { + std::cout << description << std::endl; + std::exit(0); + } else if (values.count("version")) { + std::cout << "version: " << APP_VERSION << std::endl; + std::cout << "commit: " << GIT_COMMIT_ID << std::endl; + std::cout << "compiled on: " << __DATE__ << " " << __TIME__ << std::endl; + std::exit(0); + } else if (values.count("exit")) { + // Application::requetExit(); + std::exit(0); + } + LOG(info) << "version: " << APP_VERSION << std::endl; + LOG(info) << "commit: " << GIT_COMMIT_ID << std::endl; + LOG(info) << "compiled on: " << __DATE__ << " " << __TIME__ << std::endl; + auto application = Singleton::construct(); + + boost::asio::signal_set signals(application->ioContext(), SIGINT, SIGTERM, SIGHUP); + signals.async_wait([&application](boost::system::error_code const &, int signal) { + LOG(info) << "capture " << (signal == SIGINT ? "SIGINT" : "SIGTERM") << ",stop!"; + application->exit(5); + }); + return application->exec(); + } catch (const std::exception &e) { + LOG(error) << e.what(); + return -1; + } } diff --git a/Tools/CMakeLists.txt b/Tools/CMakeLists.txt new file mode 100644 index 0000000..11feb4d --- /dev/null +++ b/Tools/CMakeLists.txt @@ -0,0 +1,8 @@ +add_executable(UrlCheck UrlCheck.cpp) + +target_link_libraries(UrlCheck + PRIVATE Base + PRIVATE Boost::program_options + PRIVATE OpenSSL::SSL + PRIVATE OpenSSL::Crypto +) \ No newline at end of file diff --git a/Tools/UrlCheck.cpp b/Tools/UrlCheck.cpp new file mode 100644 index 0000000..17877f0 --- /dev/null +++ b/Tools/UrlCheck.cpp @@ -0,0 +1,121 @@ +#include "Base/Database.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +bool isUrlValid(boost::asio::io_context &ioContext, const std::string &host, const std::string &port, const std::string &target); + +int main(int argc, char const *argv[]) { + boost::program_options::options_description description("Allowed options"); + // clang-format off + description.add_options() + ("help,h", "produce help message.") + ("database,d", boost::program_options::value(),"set database path") + ("delete-invalid", boost::program_options::value()->default_value(false),"delete invalid url"); + // clang-format on + boost::program_options::variables_map values; + boost::program_options::store(boost::program_options::parse_command_line(argc, argv, description), values); + boost::program_options::notify(values); + + std::string path; + + if (values.count("help")) { + std::cout << description << std::endl; + std::exit(0); + } else if (values.count("database")) { + path = values.at("database").as(); + } + + if (path.empty()) { + std::cerr << "please specify the database path." << std::endl; + std::cout << description << std::endl; + return 1; + } else if (!std::filesystem::exists(path)) { + std::cerr << "database file " << path << " not existed." << std::endl; + return 2; + } + + Older::Database database; + if (!database.open(path)) { + return 3; + } + + boost::asio::io_context ioContext; + auto items = database.visitRecords(); + for (auto &item : items) { + bool valid = isUrlValid(ioContext, "amass.fun", "443", item.url); + std::cout << item.url << std::endl; + std::cout << "valid: " << valid << std::endl; + if (!valid && values.at("delete-invalid").as()) { + std::cout << "delete: " << database.removeVisitRecord(item.id) << std::endl; + } + std::cout << "----------" << std::endl; + } + + return 0; +} + +bool isUrlValid(boost::asio::io_context &ioContext, const std::string &host, const std::string &port, const std::string &target) { + using namespace boost; + using namespace boost::asio; + using namespace boost::asio::ip; + using namespace boost::beast; + try { + // 1. 创建SSL上下文 + ssl::context ssl_ctx(ssl::context::tlsv12_client); + ssl_ctx.set_default_verify_paths(); + ssl_ctx.set_verify_mode(ssl::verify_peer); + + // 2. 创建TCP解析器和SSL流 + tcp::resolver resolver(ioContext); + beast::ssl_stream stream(ioContext, ssl_ctx); + + // 3. 设置SNI主机名(重要!) + if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str())) { + throw boost::system::system_error(::ERR_get_error(), boost::asio ::error::get_ssl_category()); + } + + // 4. 解析主机名并建立连接 + auto const results = resolver.resolve(host, port); + beast::get_lowest_layer(stream).connect(results); + stream.handshake(ssl::stream_base::client); + + // 5. 构造并发送HEAD请求 + http::request req{http::verb::get, target, 11}; + req.set(http::field::host, host); + req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + http::write(stream, req); + + // 6. 读取响应 + beast::flat_buffer buffer; + http::response res; + http::read(stream, buffer, res); + // 7. 检查HTTP状态码(2xx/3xx视为可访问) + const unsigned status = res.result_int(); + const bool accessible = (status >= 200 && status < 400); + + // 8. 优雅关闭连接 + beast::error_code ec; + stream.shutdown(ec); + if (ec == net::error::eof || ec == boost::asio::ssl::error::stream_truncated) { + ec = {}; + } + return accessible; + } catch (const std::exception &e) { + std::cerr << "Error: " << e.what() << std::endl; + return false; + } +}