修复: 升级框架并完善报告导出

- 升级 Drogon 和 Trantor,修复畸形请求导致的连接计数泄漏\n- 增加第三方框架版本校验与自动重建\n- 完善完整报告导出和接口文档
This commit is contained in:
cloud
2026-08-10 09:50:09 +08:00
parent 99ed321d24
commit 0e28826073
82 changed files with 3095 additions and 566 deletions
+79 -6
View File
@@ -100,14 +100,44 @@ jobs:
- name: Prepare for testing
run: |
brew services restart postgresql@14
for _ in {1..30}; do
if pg_isready -h 127.0.0.1 -p 5432 -U postgres; then
break
fi
sleep 1
done
pg_isready -h 127.0.0.1 -p 5432 -U postgres
brew services start mariadb
for _ in {1..30}; do
if mariadb-admin ping --silent; then
break
fi
sleep 1
done
mariadb-admin ping --silent
brew services start redis
sleep 4
for _ in {1..30}; do
if redis-cli ping | grep -q PONG; then
break
fi
sleep 1
done
redis-cli ping | grep -q PONG
mariadb -e "SET PASSWORD FOR 'root'@'localhost' = PASSWORD('')"
mariadb -e "GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost'"
mariadb -e "FLUSH PRIVILEGES"
brew services restart mariadb
sleep 4
for _ in {1..30}; do
if mariadb-admin ping --silent; then
break
fi
sleep 1
done
mariadb-admin ping --silent
psql -c 'create user postgres superuser;' postgres
- name: Test
@@ -178,9 +208,46 @@ jobs:
- name: Install g++
if: startsWith(matrix.compiler.cxx, 'g++') && (matrix.compiler.ver == 13 || matrix.compiler.ver == 9)
run: |
sudo add-apt-repository ppa:ubuntu-toolchain-r/test
sudo apt-get install g++-${{ matrix.compiler.ver }}
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-${{ matrix.compiler.ver }} ${{ matrix.compiler.ver }}
requested="g++-${{ matrix.compiler.ver }}"
if command -v g++-${{ matrix.compiler.ver }} >/dev/null 2>&1; then
echo "g++-${{ matrix.compiler.ver }} is already available on the runner"
else
retry() {
local attempts="$1"
shift
local try=1
while true; do
"$@" && break
if [ "$try" -ge "$attempts" ]; then
return 1
fi
echo "Command failed, retrying ($try/$attempts): $*"
try=$((try + 1))
sleep 10
done
}
if retry 3 sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test && \
retry 3 sudo apt-get update && \
retry 3 sudo apt-get install -y "$requested"; then
echo "Installed $requested from PPA"
else
echo "::warning::Failed to install $requested from Launchpad PPA (network timeout). Falling back to system g++."
if ! command -v g++ >/dev/null 2>&1; then
echo "::error::Neither $requested nor system g++ is available."
exit 1
fi
fallback_cxx="$(command -v g++)"
echo "CXX=$fallback_cxx" >> "$GITHUB_ENV"
echo "Using fallback compiler: $fallback_cxx"
exit 0
fi
fi
if command -v "$requested" >/dev/null 2>&1; then
sudo update-alternatives --install /usr/bin/g++ g++ "$(command -v "$requested")" ${{ matrix.compiler.ver }}
fi
- name: Install Clang
if: startsWith(matrix.compiler.cxx, 'clang') && matrix.compiler.ver < 13
@@ -228,7 +295,13 @@ jobs:
- name: Prepare for testing
run: |
sudo systemctl start postgresql
sleep 1
for _ in {1..30}; do
if pg_isready -h 127.0.0.1 -p 5432 -U postgres; then
break
fi
sleep 1
done
pg_isready -h 127.0.0.1 -p 5432 -U postgres
sudo -u postgres psql -c "ALTER USER postgres WITH PASSWORD '12345'" postgres
- name: Test
+2
View File
@@ -47,3 +47,5 @@ CMakeSettings.json
install
trace.json
.cache/
build_examples/
.kiro
+4 -1
View File
@@ -25,7 +25,7 @@ CMAKE_DEPENDENT_OPTION(USE_SPDLOG "Allow using the spdlog logging library" OFF "
set(DROGON_MAJOR_VERSION 1)
set(DROGON_MINOR_VERSION 9)
set(DROGON_PATCH_VERSION 12)
set(DROGON_PATCH_VERSION 13)
set(DROGON_VERSION
${DROGON_MAJOR_VERSION}.${DROGON_MINOR_VERSION}.${DROGON_PATCH_VERSION})
set(DROGON_VERSION_STRING "${DROGON_VERSION}")
@@ -134,6 +134,8 @@ if (WIN32)
PRIVATE $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/third_party/mman-win32>)
endif (WIN32)
add_library(Drogon::Drogon ALIAS ${PROJECT_NAME})
if(USE_SUBMODULE)
add_subdirectory(trantor)
target_link_libraries(${PROJECT_NAME} PUBLIC trantor)
@@ -329,6 +331,7 @@ set(private_headers
lib/src/ListenerManager.h
lib/src/PluginsManager.h
lib/src/SessionManager.h
lib/src/utils/ParsingUtils.h
lib/src/SpinLock.h
lib/src/StaticFileRouter.h
lib/src/TaskTimeoutFlag.h
+75 -1
View File
@@ -4,6 +4,78 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
## [1.9.13] - 2026-05-06
### API changes list
- Add `HttpRequest::clearHeaders()` method.
- Add `setQueryParameter()` and `setBodyParameter()` methods.
- Add ability to use `BEGIN IMMEDIATE` and `BEGIN EXCLUSIVE`.
- Add `UploadFile` constructor to create from memory data.
- Add JOIN support to Mapper/BaseBuilder with FK auto-detection in code generator.
- Add WebDAV HTTP methods (PROPFIND, MKCOL, COPY, MOVE).
- Add per-request compression control to `HttpResponse`.
### Added
- Support for custom OPTIONS handling via middleware flagging.
- Alias library for Drogon with name matching installed target.
- `--clear-output` option to drogon_ctl create models.
### Changed
- vector: reserve before inserting for efficiency.
- make `utils::isBase64` support padding.
- Extract duplicate `parseLine()` function to shared utility header.
- Doxygen documentation adjustments.
- Enhancement for custom OPTIONS handling.
- Forward the path methods.
### Fixed
- Fix sqlite3 test in CI.
- Fix HttpClient not sending WebDAV requests.
- Fix connection limit bug.
- Fix bugs exposed by CI.
- Fix HTTP date formatting to be locale-independent.
- Fix parsing invalid numbers in HTTP headers.
- Fix shared lib view failure handling.
- Fix drogon_ctl compilation with clang-cl.
- Fix missing throw statement.
- Add Homebrew Apple Silicon path detection in CMake finder modules.
- Include missing header files.
- Fix wrong numeric limit for floating types.
- Fix regex WebSocket routes middleware initialization.
- Fix system() replaced with execvp() in SharedLibManager.
- Fix inverted test logic.
## [1.9.12] - 2026-01-26
### API changes list
@@ -1878,7 +1950,9 @@ All notable changes to this project will be documented in this file.
## [1.0.0-beta1] - 2019-06-11
[Unreleased]: https://github.com/an-tao/drogon/compare/v1.9.12...HEAD
[Unreleased]: https://github.com/an-tao/drogon/compare/v1.9.13...HEAD
[1.9.13]: https://github.com/an-tao/drogon/compare/v1.9.12...v1.9.13
[1.9.12]: https://github.com/an-tao/drogon/compare/v1.9.11...v1.9.12
+15
View File
@@ -21,6 +21,21 @@
# ##############################################################################
include(FindPackageHandleStandardArgs)
# On Apple Silicon Macs, Homebrew installs to /opt/homebrew instead of
# /usr/local (Intel Macs). Detect the prefix dynamically so cmake finds
# dependencies regardless of Mac architecture.
if(APPLE)
execute_process(
COMMAND brew --prefix brotli
OUTPUT_VARIABLE HOMEBREW_BROTLI_PREFIX
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
if(HOMEBREW_BROTLI_PREFIX)
list(APPEND CMAKE_PREFIX_PATH ${HOMEBREW_BROTLI_PREFIX})
endif()
endif()
find_path(BROTLI_INCLUDE_DIR "brotli/decode.h")
find_library(BROTLICOMMON_LIBRARY NAMES brotlicommon brotlicommon-static)
+15
View File
@@ -5,6 +5,21 @@
# HIREDIS_INCLUDE_DIRS - hiredis include directories
# HIREDIS_LIBRARIES - libraries need to use hiredis
# On Apple Silicon Macs, Homebrew installs to /opt/homebrew instead of
# /usr/local (Intel Macs). Detect the prefix dynamically so cmake finds
# dependencies regardless of Mac architecture.
if(APPLE)
execute_process(
COMMAND brew --prefix hiredis
OUTPUT_VARIABLE HOMEBREW_HIREDIS_PREFIX
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
if(HOMEBREW_HIREDIS_PREFIX)
list(APPEND CMAKE_PREFIX_PATH ${HOMEBREW_HIREDIS_PREFIX})
endif()
endif()
if (HIREDIS_INCLUDE_DIRS AND HIREDIS_LIBRARIES)
set(HIREDIS_FIND_QUIETLY TRUE)
set(Hiredis_FOUND TRUE)
+16
View File
@@ -10,6 +10,22 @@
# false, do not try to use jsoncpp.
# Jsoncpp_lib - The imported target library.
# On Apple Silicon Macs, Homebrew installs to /opt/homebrew instead of
# /usr/local (Intel Macs). Detect the prefix dynamically so cmake finds
# dependencies regardless of Mac architecture.
if(APPLE)
execute_process(
COMMAND brew --prefix jsoncpp
OUTPUT_VARIABLE HOMEBREW_JSONCPP_PREFIX
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
if(HOMEBREW_JSONCPP_PREFIX)
list(APPEND CMAKE_PREFIX_PATH ${HOMEBREW_JSONCPP_PREFIX})
endif()
endif()
# only look in default directories
find_path(JSONCPP_INCLUDE_DIRS
NAMES json/json.h
+15
View File
@@ -13,6 +13,21 @@
# SQLite3_FOUND - True if sqlite3 found.
# SQLite3_lib - The imported target library.
# On Apple Silicon Macs, Homebrew installs to /opt/homebrew instead of
# /usr/local (Intel Macs). Detect the prefix dynamically so cmake finds
# dependencies regardless of Mac architecture.
if(APPLE)
execute_process(
COMMAND brew --prefix sqlite3
OUTPUT_VARIABLE HOMEBREW_SQLITE3_PREFIX
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
if(HOMEBREW_SQLITE3_PREFIX)
list(APPEND CMAKE_PREFIX_PATH ${HOMEBREW_SQLITE3_PREFIX})
endif()
endif()
# Look for the header file.
find_path(SQLITE3_INCLUDE_DIRS NAMES sqlite3.h)
+15
View File
@@ -7,6 +7,21 @@
# PostgreSQL.
# pg_lib - The imported target library.
# On Apple Silicon Macs, Homebrew installs to /opt/homebrew instead of
# /usr/local (Intel Macs). Detect the prefix dynamically so cmake finds
# dependencies regardless of Mac architecture.
if(APPLE)
execute_process(
COMMAND brew --prefix libpq
OUTPUT_VARIABLE HOMEBREW_PG_PREFIX
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
if(HOMEBREW_PG_PREFIX)
list(APPEND CMAKE_PREFIX_PATH ${HOMEBREW_PG_PREFIX})
endif()
endif()
find_package(PostgreSQL)
if(PostgreSQL_FOUND)
set(PG_LIBRARIES ${PostgreSQL_LIBRARIES})
+13 -1
View File
@@ -42,7 +42,8 @@ std::string create::detail()
"create a plugin named class_name\n\n"
"drogon_ctl create project <project_name> //"
"create a project named project_name\n\n"
"drogon_ctl create model <model_path> [-o <output path>] "
"drogon_ctl create model <model_path> [-o <output path>] [ "
"--clear-output]"
"[--table=<table_name>] [-f]//"
"create model classes in model_path\n";
}
@@ -55,3 +56,14 @@ void create::handleCommand(std::vector<std::string> &parameters)
parameters[0] = createObjName;
exeCommand(parameters);
}
// Prevent clang-cl/lld-link from discarding DrObject<T>::alloc_ on Windows.
// On COFF targets, clang places the CRT initializer for the template static
// member in the same COMDAT group as the variable itself. When no code in the
// translation unit takes the address of alloc_ (clang inlines className()),
// the entire COMDAT is eligible for elimination — and lld-link's /OPT:REF
// removes it, so DrClassMap is never populated.
// Explicit template instantiation forces a strong (non-COMDAT) definition,
// which the linker must keep. This is a no-op on MSVC, GCC, and ELF targets
// (where .init_array entries are GC roots and are never discarded).
template class drogon::DrObject<drogon_ctl::create>;
@@ -469,3 +469,6 @@ void create_controller::createARestfulController(const std::string &className,
std::cout << "File name: " << ctlName << ".h and " << ctlName << ".cc"
<< std::endl;
}
// See create.cc for rationale.
template class drogon::DrObject<drogon_ctl::create_controller>;
+3
View File
@@ -116,3 +116,6 @@ void create_filter::handleCommand(std::vector<std::string> &parameters)
createFilterSourceFile(oSourceFile, className, fileName);
}
}
// See create.cc for rationale.
template class drogon::DrObject<drogon_ctl::create_filter>;
+195 -3
View File
@@ -164,6 +164,59 @@ bool drogon_ctl::ConvertMethod::shouldConvert(const std::string &tableName,
} // endif
}
/**
* @brief Try to add an auto-detected FK relationship to the list.
*
* Checks for duplicates against existing relationships, creates a
* Relationship object from the FK info, and appends it to the list.
* User-configured relationships always take priority.
*
* @param allRelationships The mutable vector of relationships.
* @param originalTable The table containing the FK column.
* @param fkColumn The FK column name.
* @param referencedTable The table referenced by the FK.
* @param referencedColumn The column referenced by the FK.
* @param normalizeNames If true, apply toLower() to table names.
*/
static void tryAddAutoRelationship(std::vector<Relationship> &allRelationships,
const std::string &originalTable,
const std::string &fkColumn,
const std::string &referencedTable,
const std::string &referencedColumn,
bool normalizeNames)
{
for (const auto &r : allRelationships)
{
if (r.originalKey() == fkColumn &&
r.targetTableName() == referencedTable)
{
return; // Already exists in user config
}
}
Json::Value relJson;
relJson["type"] = "has one";
relJson["original_table_name"] =
normalizeNames ? toLower(originalTable) : originalTable;
relJson["original_key"] = fkColumn;
relJson["target_table_name"] =
normalizeNames ? toLower(referencedTable) : referencedTable;
relJson["target_key"] = referencedColumn;
relJson["enable_reverse"] = true;
try
{
Relationship autoRel(relJson);
allRelationships.push_back(autoRel);
std::cout << " Auto-detected FK: " << originalTable << "."
<< fkColumn << " -> " << referencedTable << "."
<< referencedColumn << std::endl;
}
catch (const std::runtime_error &e)
{
std::cerr << "Warning: Could not create auto-relationship: " << e.what()
<< std::endl;
}
}
#if USE_POSTGRESQL
void create_model::createModelClassFromPG(
const std::string &path,
@@ -182,8 +235,9 @@ void create_model::createModelClassFromPG(
data["primaryKeyName"] = "";
data["dbName"] = dbname_;
data["rdbms"] = std::string("postgresql");
data["relationships"] = relationships;
data["convertMethods"] = convertMethods;
// Start with user-configured relationships (mutable copy)
std::vector<Relationship> allRelationships(relationships);
if (schema != "public")
{
data["schema"] = schema;
@@ -397,6 +451,42 @@ void create_model::createModelClassFromPG(
data["primaryKeyValNames"] = pkValNames;
}
// Auto-detect foreign key relationships from database schema
*client << "SELECT "
"kcu.column_name AS fk_column, "
"ccu.table_name AS referenced_table, "
"ccu.column_name AS referenced_column "
"FROM information_schema.key_column_usage kcu "
"JOIN information_schema.referential_constraints rc "
"ON kcu.constraint_name = rc.constraint_name "
"AND kcu.constraint_schema = rc.constraint_schema "
"JOIN information_schema.constraint_column_usage ccu "
"ON rc.unique_constraint_name = ccu.constraint_name "
"AND rc.unique_constraint_schema = ccu.constraint_schema "
"WHERE kcu.table_name = $1 "
"AND kcu.table_schema = $2"
<< tableName << schema << Mode::Blocking >>
[&](bool isNull,
const std::string &fkColumn,
const std::string &referencedTable,
const std::string &referencedColumn) {
if (!isNull)
{
tryAddAutoRelationship(allRelationships,
tableName,
fkColumn,
referencedTable,
referencedColumn,
true);
}
} >>
[](const DrogonDbException &e) {
// FK detection is best-effort; don't fail if unsupported
std::cerr << "Note: FK auto-detection not available: "
<< e.base().what() << std::endl;
};
data["relationships"] = allRelationships;
data["columns"] = cols;
std::ofstream headerFile(path + "/" + className + ".h", std::ofstream::out);
std::ofstream sourceFile(path + "/" + className + ".cc",
@@ -467,8 +557,9 @@ void create_model::createModelClassFromMysql(
data["primaryKeyName"] = "";
data["dbName"] = dbname_;
data["rdbms"] = std::string("mysql");
data["relationships"] = relationships;
data["convertMethods"] = convertMethods;
// Start with user-configured relationships (mutable copy)
std::vector<Relationship> allRelationships(relationships);
std::vector<ColumnInfo> cols;
int i = 0;
*client << "desc `" + tableName + "`" << Mode::Blocking >>
@@ -593,6 +684,35 @@ void create_model::createModelClassFromMysql(
data["primaryKeyType"] = pkTypes;
data["primaryKeyValNames"] = pkValNames;
}
// Auto-detect foreign key relationships from MySQL schema
*client << "SELECT COLUMN_NAME, REFERENCED_TABLE_NAME, "
"REFERENCED_COLUMN_NAME "
"FROM information_schema.KEY_COLUMN_USAGE "
"WHERE TABLE_SCHEMA = DATABASE() "
"AND TABLE_NAME = ? "
"AND REFERENCED_TABLE_NAME IS NOT NULL"
<< tableName << Mode::Blocking >>
[&](bool isNull,
const std::string &fkColumn,
const std::string &referencedTable,
const std::string &referencedColumn) {
if (!isNull)
{
tryAddAutoRelationship(allRelationships,
tableName,
fkColumn,
referencedTable,
referencedColumn,
true);
}
} >>
[](const DrogonDbException &e) {
std::cerr << "Note: FK auto-detection not available: "
<< e.base().what() << std::endl;
};
data["relationships"] = allRelationships;
data["columns"] = cols;
std::ofstream headerFile(path + "/" + className + ".h", std::ofstream::out);
std::ofstream sourceFile(path + "/" + className + ".cc",
@@ -646,8 +766,9 @@ void create_model::createModelClassFromSqlite3(
data["primaryKeyName"] = "";
data["dbName"] = std::string("sqlite3");
data["rdbms"] = std::string("sqlite3");
data["relationships"] = relationships;
data["convertMethods"] = convertMethods;
// Start with user-configured relationships (mutable copy)
std::vector<Relationship> allRelationships(relationships);
std::vector<ColumnInfo> cols;
std::string sql = "PRAGMA table_info(" + tableName + ");";
*client << sql << Mode::Blocking >> [&](const Result &result) {
@@ -774,6 +895,28 @@ void create_model::createModelClassFromSqlite3(
data["primaryKeyType"] = pkTypes;
data["primaryKeyValNames"] = pkValNames;
}
// Auto-detect foreign key relationships from SQLite3 schema
std::string fkSql = "PRAGMA foreign_key_list(\"" + tableName + "\");";
*client << fkSql << Mode::Blocking >> [&](const Result &fkResult) {
for (auto &fkRow : fkResult)
{
auto referencedTable = fkRow["table"].as<std::string>();
auto fkColumn = fkRow["from"].as<std::string>();
auto referencedColumn = fkRow["to"].as<std::string>();
tryAddAutoRelationship(allRelationships,
tableName,
fkColumn,
referencedTable,
referencedColumn,
true);
}
} >> [](const DrogonDbException &e) {
std::cerr << "Note: FK auto-detection not available: "
<< e.base().what() << std::endl;
};
data["relationships"] = allRelationships;
data["columns"] = cols;
std::ofstream headerFile(path + "/" + className + ".h", std::ofstream::out);
std::ofstream sourceFile(path + "/" + className + ".cc",
@@ -826,7 +969,42 @@ void create_model::createModel(const std::string &path,
auto restfulApiConfig = config["restful_api_controllers"];
auto relationships = getRelationships(config["relationships"]);
auto convertMethods = getConvertMethods(config["convert"]);
drogon::utils::createPath(path);
if (cleanupDirectory_)
{
std::cout << "Source files (*.h, *.cc) in '" << path
<< "' folder will be deleted, continue(y/n)?\n";
auto in = getchar();
(void)getchar(); // get the return key
if (in != 'Y' && in != 'y')
{
std::cout << "Abort!" << std::endl;
exit(0);
}
for (const auto &entry : std::filesystem::directory_iterator(path))
{
if (!entry.is_regular_file())
continue;
const std::filesystem::path &file = entry.path();
std::string ext = file.extension().string();
if (ext == ".h" || ext == ".cc")
{
std::cout << "Removing: " << file << "\n";
std::error_code ret;
std::filesystem::remove(file, ret);
if (ret)
{
std::cerr << "Failed to remove '" << file
<< "' : " << ret.message() << "\n";
}
}
}
}
if (dbType == "postgresql")
{
#if USE_POSTGRESQL
@@ -1230,6 +1408,17 @@ void create_model::handleCommand(std::vector<std::string> &parameters)
++iter;
}
for (auto iter = parameters.begin(); iter != parameters.end(); ++iter)
{
if ((*iter) == "--clear-output")
{
cleanupDirectory_ = true;
forceOverwrite_ = true;
parameters.erase(iter);
break;
}
}
for (auto const &path : parameters)
{
createModel(path, singleModelName);
@@ -1414,3 +1603,6 @@ void create_model::createRestfulAPIController(
<< std::endl;
}
}
// See create.cc for rationale.
template class drogon::DrObject<drogon_ctl::create_model>;
+1
View File
@@ -430,5 +430,6 @@ class create_model : public DrObject<create_model>, public CommandHandler
std::string dbname_;
bool forceOverwrite_{false};
std::string outputPath_;
bool cleanupDirectory_{false};
};
} // namespace drogon_ctl
+3
View File
@@ -116,3 +116,6 @@ void create_plugin::handleCommand(std::vector<std::string> &parameters)
createPluginSourceFile(oSourceFile, className, fileName);
}
}
// See create.cc for rationale.
template class drogon::DrObject<drogon_ctl::create_plugin>;
+3
View File
@@ -141,3 +141,6 @@ void create_project::createProject(const std::string &projectName)
std::ofstream testCmakeFile("test/CMakeLists.txt", std::ofstream::out);
newTestCmakeFile(testCmakeFile, projectName);
}
// See create.cc for rationale.
template class drogon::DrObject<drogon_ctl::create_project>;
+3
View File
@@ -552,3 +552,6 @@ void create_view::newViewSourceFile(std::ofstream &file,
file << "return templ->genText(data);\n";
file << "}\n}\n";
}
// See create.cc for rationale.
template class drogon::DrObject<drogon_ctl::create_view>;
+3
View File
@@ -68,3 +68,6 @@ void help::handleCommand(std::vector<std::string> &parameters)
}
}
}
// See create.cc for rationale.
template class drogon::DrObject<drogon_ctl::help>;
+3
View File
@@ -470,3 +470,6 @@ void press::outputResults()
<< std::endl;
exit(0);
}
// See create.cc for rationale.
template class drogon::DrObject<drogon_ctl::press>;
+3
View File
@@ -66,3 +66,6 @@ void version::handleCommand(std::vector<std::string> &parameters)
std::cout << " yaml-cpp: no\n";
#endif
}
// See create.cc for rationale.
template class drogon::DrObject<drogon_ctl::version>;
+42
View File
@@ -148,4 +148,46 @@ class HttpCoroMiddleware : public DrObject<T>, public HttpMiddlewareBase
#endif
/**
* @brief Simple middleware that tags OPTIONS requests
* @details It adds the attribute "drogon.customCORShandling" to the request, so
* that HttpServer does not handle CORS for them internally.
*
* This allows custom CORS handling via the path handlers.
* For example to restrict the origins, headers allowed, specify a max age to
* avoid OPTIONS on every request, etc.
*
* Just register it:
* 1. globally via
* app().registerMiddleware(std::make_shared<drogon::HttpOptionsMiddleware>())
* 2. on every path handlers that need non-default handling, with
* ADD_METHOD_TO(..., drogon::Options, "drogon::HttpOptionsMiddleware")
*/
template <class Derived, bool AutoCreation = true>
class HttpOptionsMiddlewareImpl
: public drogon::HttpMiddleware<Derived, AutoCreation>
{
public:
void invoke(const HttpRequestPtr &req,
MiddlewareNextCallback &&nextCb,
MiddlewareCallback &&mcb) override
{
// Tag OPTIONS
if (req->method() == drogon::HttpMethod::Options)
req->attributes()->insert("drogon.customCORShandling", true);
// continue with next middleware (no post-processing here)
nextCb(std::move(mcb));
}
};
class HttpOptionsMiddlewareAuto
: public HttpOptionsMiddlewareImpl<HttpOptionsMiddlewareAuto, true>
{
};
class HttpOptionsMiddleware
: public HttpOptionsMiddlewareImpl<HttpOptionsMiddleware, false>
{
};
} // namespace drogon
+50
View File
@@ -159,6 +159,9 @@ class DROGON_EXPORT HttpRequest
*/
virtual void removeHeader(std::string key) = 0;
// Clear all HTTP headers
virtual void clearHeaders() = 0;
/// Get the cookie string identified by the field parameter
virtual const std::string &getCookie(const std::string &field) const = 0;
@@ -415,6 +418,8 @@ class DROGON_EXPORT HttpRequest
virtual void setMethod(const HttpMethod method) = 0;
/// Set the path of the request
/// @note The path is automatically encoded. use
/// @c setPathEncode(false) to avoid this.
virtual void setPath(const std::string &path) = 0;
virtual void setPath(std::string &&path) = 0;
@@ -432,6 +437,20 @@ class DROGON_EXPORT HttpRequest
virtual void setParameter(const std::string &key,
const std::string &value) = 0;
/**
* Set the parameter to the query,
* regardless of the HTTP method or content type
*/
virtual void setQueryParameter(const std::string &key,
const std::string &value) = 0;
/**
* Set the parameter to the request body.
* @warning The content type must be @c application/x-www-form-urlencoded
* or @c multipart/form-data
*/
virtual void setBodyParameter(const std::string &key,
const std::string &value) = 0;
/// Set or get the content type
virtual void setContentTypeCode(const ContentType type) = 0;
@@ -501,6 +520,37 @@ class DROGON_EXPORT HttpRequest
return toRequest(std::forward<T>(obj));
}
/*! \brief Check if the request is a CORS request.
* \details It should contain:
* - Origin: origination page
* \returns true if the Origin header is present
*/
inline bool isCorsRequest() const
{
// Check presence of required headers
return headers().find("origin") != headers().end();
}
/*! \brief Check if the request is a CORS pre-flight request.
* \details Check if the method of the request is OPTIONS and if it is
* a CORS pre-flight request.\n
* It should contain:
* - Origin: origination page
* - Access-Control-Request-Method: method to be used in the
* actual request
* \returns true if the method is OPTIONS and the required CORS pre-flight
* headers are present
*/
inline bool isCorsPreflightRequest() const
{
if (method() != HttpMethod::Options)
return false;
// Check presence of required headers
return isCorsRequest() &&
headers().find("access-control-request-method") !=
headers().end();
}
virtual bool isOnSecureConnection() const noexcept = 0;
virtual void setContentTypeString(const char *typeString,
size_t typeStringLength) = 0;
+143 -2
View File
@@ -161,6 +161,12 @@ class DROGON_EXPORT HttpResponse
setCustomStatusCode(code, message.data(), message.length());
}
/// Set whether the response should be compress.
virtual void setAllowCompression(bool allow) = 0;
/// Get whether the response allow compression.
virtual bool allowCompression() const = 0;
/// Get the creation timestamp of the response.
virtual const trantor::Date &creationDate() const = 0;
@@ -552,6 +558,141 @@ class DROGON_EXPORT HttpResponse
return toResponse(std::forward<T>(obj));
}
/*! \brief Create an OPTIONS or CORS pre-flight response
* \details If the request is not an OPTIONS request, returns a NULL
* response\n
* If it is a generic OPTIONS request, returns a 204 No Content
* response with the Allow header\n
* If it is a CORS pre-flight request, returns a 204 No Content
* response with the CORS headers set
*
* Other status codes for CORS pre-flight answers:
* - 400 Bad Request: if the request is malformed (missing
* required headers)
* - 403 Forbidden: if the Origin is not allowed + reason
* in a X-Cors-Error header
* - 403 Forbidden: if one of the headers in
* Access-Control-Request-Headers is not allowed + reason in
* a X-Cors-Error header
* - 405 Method Not Allowed: if the requested method is
* not allowed
* \note CORS is a browser-side security mechanism.\n
* Do not rely on Origin for authentication/authorization:
* non-browser clients can spoof or omit it.\n
* Enforce access control independently.
* \param[in] request Drogon (OPTIONS) request
* \param[in] allowedHeaders Set of allowed headers (for
* Access-Control-Allow-Headers header)\n
* (headers allowed by the controller path
* handler)
* \param[in] originValidator Function to validate the Origin header value
* (allow the origin or not)\n
* If allowCredentials is true, originValidator
* _SHOULD_ enforce a strict allowlist
* \param[in] allowNullOrigin Should be true to accept the "Origin: null"
* header\n
* (set for local file:// pages, sandboxed
* iframes, opaque origins, data: URIs)
* \param[in] allowCredentials Should be true to add the header
* "Access-Control-Allow-Credentials: true"
* (controls whether the browser may include
* credentials such as cookies, HTTP auth, or
* client certificates)\n
* Note: Authorization (bearer) is not a
* credential header; allow it via
* allowedHeaders when needed
* \param[in] allowPNA Should be true to accept the header
* "Access-Control-Request-Private-Network"
* (when a page from a less private address
* space is trying to reach a more private
* one, like internet -> intranet)\n
* Note: specific to Chromium & derivatives
* (Edge, Opera, Brave, ...), not in Firefox
* or Safari
* \param[in] maxAgeSeconds If set, adds the "Access-Control-Max-Age"
* header with the given value (in seconds,
* how long the results of a preflight
* request can be cached by the navigator)
* \returns the OPTIONS or CORS pre-flight response, or a null pointer if
* the request is not an OPTIONS request
*/
static HttpResponsePtr newOptionsResponse(
const HttpRequestPtr &request,
const std::function<bool(std::string_view)> &originValidator = nullptr,
bool allowNullOrigin = false,
bool allowCredentials = false,
bool allowPNA = true,
std::optional<unsigned int> maxAgeSeconds = {},
const std::optional<std::set<std::string_view>> &allowedHeaders =
std::nullopt);
/*! \copydoc newOptionsResponse(const HttpRequestPtr&,
* const std::function<bool(std::string_view)>&,
* bool, bool, bool,
* std::optional<unsigned int>,
* const std::optional<std::set<std::string_view>>&)
* \remarks Helper when specifying the allowed headers, when other
* parameters may be default, to avoid having to specify them all
*/
inline static HttpResponsePtr newOptionsResponse(
const HttpRequestPtr &request,
const std::set<std::string_view> &allowedHeaders,
const std::function<bool(std::string_view)> &originValidator = nullptr,
bool allowNullOrigin = false,
bool allowCredentials = false,
bool allowPNA = true,
std::optional<unsigned int> maxAgeSeconds = {})
{
return newOptionsResponse(request,
originValidator,
allowNullOrigin,
allowCredentials,
allowPNA,
maxAgeSeconds,
allowedHeaders);
}
/*! \brief Add CORS headers to a response
* \details Adds the CORS headers to a response for a normal request (a
* CORS request but not a CORS preflight request):
* - does nothing if it's an OPTIONS request, or
* - if it's not a CORS request, or
* - if it's a CORS preflight request
* Else:
* - adds Access-Control-Allow-Origin (if not yet present)
* - adds Origin to the Vary header,
* - sets or clears Access-Control-Allow-Credentials (if
* allowCredentials is set)
* - completes Access-Control-Expose-Headers
* \param[in] request Drogon request (to get Origin)
* \param[in] allowCredentials If set and true, adds the
* "Access-Control-Allow-Credentials: true
* header"\n
* If set and false, removes the
* "Access-Control-Allow-Credentials" header\n
* If not set, leaves the
* "Access-Control-Allow-Credentials" header
* untouched\n
* *MUST MATCH THE newOptionsResponse()
* PRE-FLIGHT RESPONSE VALUE*
* \param[in] exposedHeaders Set of exposed headers (for
* Access-Control-Expose-Headers header)\n
* These are the headers allowed to be exposed
* to javascript by the remote browser\n
* Note: they are *APPENDED* to any already
* present in the response, they are not
* REPLACED.\n
* This allows to complete them in the
* controller path handler.\n
* If you want to REPLACE them, remove the
* header before calling this function.
* \note may be use both in the controller path handler and in a
* pre-sending advice
*/
void addCorsHeaders(const HttpRequestPtr &request,
const std::set<std::string_view> &exposedHeaders = {},
const std::optional<bool> &allowCredentials = {});
/**
* @brief If the response is a file response (i.e. created by
* newFileResponse) returns the path on the filesystem. Otherwise a
@@ -560,9 +701,9 @@ class DROGON_EXPORT HttpResponse
virtual const std::string &sendfileName() const = 0;
/**
* @brief Returns the range of the file response as a pair ot size_t
* @brief Returns the range of the file response as a pair of size_t
* (offset, length). Length of 0 means the entire file is sent. Behavior of
* this function is undefined if the response if not a file response
* this function is undefined if the response is not a file response
*/
using SendfileRange = std::pair<size_t, size_t>; // { offset, length }
virtual const SendfileRange &sendfileRange() const = 0;
+12
View File
@@ -195,6 +195,10 @@ enum HttpMethod
Delete,
Options,
Patch,
Propfind,
Mkcol,
Copy,
Move,
Invalid
};
@@ -280,6 +284,14 @@ inline std::string_view to_string_view(drogon::HttpMethod method)
return "OPTIONS";
case drogon::HttpMethod::Patch:
return "PATCH";
case drogon::HttpMethod::Propfind:
return "PROPFIND";
case drogon::HttpMethod::Mkcol:
return "MKCOL";
case drogon::HttpMethod::Copy:
return "COPY";
case drogon::HttpMethod::Move:
return "MOVE";
default:
return "INVALID";
}
+33
View File
@@ -56,6 +56,27 @@ class UploadFile
}
}
/// Constructor
/**
* @param data Pointer to the data
* @param len Data length in bytes
* @param fileName The file name provided to the server.
* @param itemName The item name on the browser form.
* @param contentType The Mime content type for the part
*/
explicit UploadFile(const void *data,
const size_t len,
const std::string &fileName = "memory.bin",
const std::string &itemName = "file",
ContentType contentType = CT_APPLICATION_OCTET_STREAM)
: data_(data),
len_(len),
fileName_(fileName),
itemName_(itemName),
contentType_(contentType)
{
}
const std::string &path() const
{
return path_;
@@ -76,7 +97,19 @@ class UploadFile
return contentType_;
}
const void *data() const
{
return data_;
}
size_t dataLength() const
{
return len_;
}
private:
const void *data_ = nullptr;
size_t len_ = 0;
std::string path_;
std::string fileName_;
std::string itemName_;
@@ -11,6 +11,7 @@
#include <drogon/plugins/Plugin.h>
#include <trantor/utils/AsyncFileLogger.h>
#include <vector>
#include <regex>
namespace drogon
{
+162 -2
View File
@@ -124,6 +124,165 @@ DROGON_EXPORT std::set<std::string> splitStringToSet(
const std::string &str,
const std::string &separator);
/*! \brief Compare two string_views for equality, ignoring case.
* \warning This is locale dependent
* \param[in] str1 The first string_view.
* \param[in] str2 The second string_view.
* \return true if the string_views are equal, ignoring case; false otherwise.
*/
inline bool ci_equals(std::string_view str1, std::string_view str2)
{
if (str1.size() != str2.size())
return false;
return std::equal(str1.begin(),
str1.end(),
str2.begin(),
[](unsigned char a, unsigned char b) {
return std::tolower(a) == std::tolower(b);
});
}
/*! \details Trim leading and trailing spaces and tabs from a string_view,
* modifying it.
* \param[in,out] str The string_view to trim.
* \return The trimmed string_view.
*/
inline std::string_view &trim_inplace(std::string_view &str)
{
auto pos = str.find_first_not_of(" \t");
// defeat Windows macro "min"
str.remove_prefix((std::min)(pos, str.size()));
if (str.empty())
return str;
pos = str.find_last_not_of(" \t");
str.remove_suffix(str.size() - pos - 1);
return str;
}
/*! \brief Trim leading and trailing spaces and tabs from a string_view.
* \param[in] str The string_view to trim.
* \return A string_view with leading and trailing spaces and tabs removed.
*/
inline std::string_view trim(std::string_view str)
{
return trim_inplace(str);
}
/*! \brief Trim leading and trailing spaces and tabs from a rvalue string.
* \param[in] str The string to trim.
* \return The string with leading and trailing spaces and tabs removed.
*/
inline std::string trim(std::string &&str)
{
auto pos = str.find_last_not_of(" \t");
if (pos == std::string::npos)
return {};
str.resize(pos + 1);
pos = str.find_first_not_of(" \t");
if (pos > 0)
str.erase(0, pos);
return str;
}
/*! \brief Split a string_view into a vector of string_views.
* \param[in] str The string_view to split.
* \param[in] separator The separator to use for splitting.
* \param[in] trimValues Whether to trim whitespace from the resulting
* string_views.
* \param[in] acceptEmptyString Whether to include empty strings in the result.
* \return A vector of string_views obtained by splitting the input
* string_view.
*/
inline std::vector<std::string_view> splitStringView(
std::string_view str,
std::string_view separator,
bool trimValues = true,
bool acceptEmptyString = false)
{
std::vector<std::string_view> result;
if (separator.empty())
{
if (trimValues)
trim_inplace(str);
if (acceptEmptyString || !str.empty())
result.push_back(str);
return result;
}
size_t start = 0;
size_t end = 0;
while ((end = str.find(separator, start)) != std::string_view::npos)
{
auto token = str.substr(start, end - start);
if (trimValues)
trim_inplace(token);
if (acceptEmptyString || !token.empty())
result.push_back(token);
start = end + separator.size();
}
auto token = str.substr(start);
if (trimValues)
trim_inplace(token);
if (acceptEmptyString || !token.empty())
{
result.push_back(token);
}
return result;
}
/*! \brief Split a string_view into a set of string_views.
* \copyparams splitStringView
* \return A set of (unique) string_views obtained by splitting the input
* string_view.
* \note Uniqueness is case-sensitive: "A" and "a" are considered different
* values.
*/
inline std::set<std::string_view> splitStringViewToSet(
std::string_view str,
std::string_view separator,
bool trimValues = true,
bool acceptEmptyString = false)
{
auto v = splitStringView(str, separator, trimValues, acceptEmptyString);
return std::set<std::string_view>(v.begin(), v.end());
}
/*! \brief Join a vector of string_view into a string.
* \param[in] strs The vector of string_views to join.
* \param[in] separator The separator to use between string_views.
* \return A single string obtained by joining the input string_views with the
* specified separator.
* \note Empty values are skipped.
*/
inline std::string joinStringViews(const std::vector<std::string_view> &strs,
std::string_view separator)
{
std::string result;
for (std::string_view str : strs)
{
if (trim_inplace(str).empty())
continue;
if (!result.empty())
result.append(separator);
result.append(str);
}
return result;
}
/*! \brief Join a set of string_view into a string.
* \param[in] strs The set of string_views to join.
* \param[in] separator The separator to use between string_views.
* \return A single string obtained by joining the input string_views with the
* specified separator.
* \note Empty values are skipped.
*/
inline std::string joinStringViews(const std::set<std::string_view> &strs,
std::string_view separator)
{
return joinStringViews(std::vector<std::string_view>{strs.begin(),
strs.end()},
separator);
}
/// Get UUID string.
DROGON_EXPORT std::string getUuid(bool lowercase = true);
@@ -497,7 +656,8 @@ T fromString(const std::string &p) noexcept(false)
// ("1a" should not return 1)
if (pos != p.size())
throw std::invalid_argument("Invalid value");
if ((v < static_cast<long double>((std::numeric_limits<T>::min)())) ||
if ((v <
static_cast<long double>((std::numeric_limits<T>::lowest)())) ||
(v > static_cast<long double>((std::numeric_limits<T>::max)())))
throw std::out_of_range("Value out of range");
return static_cast<T>(v);
@@ -516,7 +676,7 @@ T fromString(const std::string &p) noexcept(false)
// throw if the whole string could not be parsed
// ("1a" should not return 1)
if (!ss.eof())
std::runtime_error("Bad type conversion");
throw std::runtime_error("Bad type conversion");
}
return value;
}
@@ -57,12 +57,6 @@ void HttpConnectionLimit::releaseConnection(
const trantor::TcpConnectionPtr &conn)
{
assert(!conn->connected());
if (!conn->hasContext())
{
// If the connection is connected to the SSL port and then
// disconnected before the SSL handshake.
return;
}
connectionNum_.fetch_sub(1, std::memory_order_relaxed);
if (maxConnectionNumPerIP_ > 0)
{
@@ -63,6 +63,11 @@ void HttpControllersRouter::init(
initMiddlewaresAndCorsMethods(iter.second);
}
for (auto &router : wsCtrlVector_)
{
initMiddlewaresAndCorsMethods(router);
}
for (auto &router : ctrlVector_)
{
router.regex_ = std::regex(router.pathParameterPattern_,
@@ -85,6 +90,7 @@ void HttpControllersRouter::reset()
ctrlMap_.clear();
ctrlVector_.clear();
wsCtrlMap_.clear();
wsCtrlVector_.clear();
}
std::vector<HttpHandlerInfo> HttpControllersRouter::getHandlersInfo() const
+73 -12
View File
@@ -215,6 +215,18 @@ void HttpRequestImpl::appendToBuffer(trantor::MsgBuffer *output) const
case Patch:
output->append("PATCH ");
break;
case Propfind:
output->append("PROPFIND ");
break;
case Mkcol:
output->append("MKCOL ");
break;
case Copy:
output->append("COPY ");
break;
case Move:
output->append("MOVE ");
break;
default:
return;
}
@@ -236,7 +248,7 @@ void HttpRequestImpl::appendToBuffer(trantor::MsgBuffer *output) const
}
std::string content;
if (passThrough_ && !query_.empty())
if (!query_.empty())
{
output->append("?");
output->append(query_);
@@ -323,21 +335,31 @@ void HttpRequestImpl::appendToBuffer(trantor::MsgBuffer *output) const
content.append(type.data(), type.length());
}
content.append("\r\n\r\n");
std::ifstream infile(utils::toNativePath(file.path()),
std::ifstream::binary);
if (!infile)
if (file.data() && file.dataLength() > 0)
{
LOG_ERROR << file.path() << " not found";
content.append((const char *)file.data(),
file.dataLength());
}
else
{
std::streambuf *pbuf = infile.rdbuf();
std::streamsize filesize = pbuf->pubseekoff(0, infile.end);
pbuf->pubseekoff(0, infile.beg); // rewind
std::string str;
str.resize(filesize);
pbuf->sgetn(&str[0], filesize);
content.append(std::move(str));
std::ifstream infile(utils::toNativePath(file.path()),
std::ifstream::binary);
if (!infile)
{
LOG_ERROR << file.path() << " not found";
}
else
{
std::streambuf *pbuf = infile.rdbuf();
std::streamsize filesize =
pbuf->pubseekoff(0, infile.end);
pbuf->pubseekoff(0, infile.beg); // rewind
std::string str;
str.resize(filesize);
pbuf->sgetn(&str[0], filesize);
content.append(std::move(str));
}
}
content.append("\r\n");
}
@@ -648,6 +670,18 @@ const char *HttpRequestImpl::methodString() const
case Patch:
result = "PATCH";
break;
case Propfind:
result = "PROPFIND";
break;
case Mkcol:
result = "MKCOL";
break;
case Copy:
result = "COPY";
break;
case Move:
result = "MOVE";
break;
default:
break;
}
@@ -683,6 +717,14 @@ bool HttpRequestImpl::setMethod(const char *start, const char *end)
{
method_ = Head;
}
else if (m == "COPY")
{
method_ = Copy;
}
else if (m == "MOVE")
{
method_ = Move;
}
else
{
method_ = Invalid;
@@ -693,6 +735,10 @@ bool HttpRequestImpl::setMethod(const char *start, const char *end)
{
method_ = Patch;
}
else if (m == "MKCOL")
{
method_ = Mkcol;
}
else
{
method_ = Invalid;
@@ -718,6 +764,16 @@ bool HttpRequestImpl::setMethod(const char *start, const char *end)
method_ = Invalid;
}
break;
case 8:
if (m == "PROPFIND")
{
method_ = Propfind;
}
else
{
method_ = Invalid;
}
break;
default:
method_ = Invalid;
break;
@@ -753,6 +809,11 @@ void HttpRequestImpl::reserveBodySize(size_t length)
{
// Store data of body to a temporary file
createTmpFile();
if (!content_.empty())
{
cacheFilePtr_->append(content_);
content_.clear();
}
}
}
+26
View File
@@ -351,6 +351,11 @@ class HttpRequestImpl : public HttpRequest
headers_.erase(lowerKey);
}
void clearHeaders() override
{
headers_.clear();
}
const std::string &getHeader(std::string field) const override
{
std::transform(field.begin(),
@@ -408,6 +413,27 @@ class HttpRequestImpl : public HttpRequest
parameters_[key] = value;
}
void setQueryParameter(const std::string &key,
const std::string &value) override
{
if (!query_.empty())
{
query_.append("&");
}
query_.append(utils::urlEncodeComponent(key));
query_.append("=");
query_.append(utils::urlEncodeComponent(value));
}
void setBodyParameter(const std::string &key,
const std::string &value) override
{
assert(contentType_ == CT_MULTIPART_FORM_DATA ||
contentType_ == CT_APPLICATION_X_FORM);
flagForParsingParameters_ = true;
parameters_[key] = value;
}
const std::string &getContent() const
{
return content_;
+220
View File
@@ -54,6 +54,16 @@ static inline HttpResponsePtr genHttpResponse(const std::string &viewName,
}
} // namespace drogon
void HttpResponseImpl::setAllowCompression(bool allow)
{
allowCompression_ = allow;
}
bool HttpResponseImpl::allowCompression() const
{
return allowCompression_;
}
HttpResponsePtr HttpResponse::newHttpResponse()
{
auto res = std::make_shared<HttpResponseImpl>(k200OK, CT_TEXT_HTML);
@@ -473,6 +483,210 @@ HttpResponsePtr HttpResponse::newAsyncStreamResponse(
return resp;
}
HttpResponsePtr HttpResponse::newOptionsResponse(
const HttpRequestPtr &request,
const std::function<bool(std::string_view)> &originValidator,
bool allowNullOrigin,
bool allowCredentials,
bool allowPNA,
std::optional<unsigned int> maxAgeSeconds,
const std::optional<std::set<std::string_view>> &allowedHeaders)
{
if (!request || (request->method() != HttpMethod::Options))
return {};
// Allowed methods, set by drogon::HttpOptionsMiddlewareImpl
auto methods =
request->attributes()->get<std::string>("drogon.corsMethods");
if (methods.empty())
methods = "OPTIONS";
auto response = newHttpResponse(HttpStatusCode::k204NoContent,
drogon::ContentType::CT_NONE);
// Disable HTTP caching for OPTIONS responses
response->addHeader("Cache-Control"s, "no-store"s);
// Vary on Origin for bad proxies that do not respect no-store or want
// Pragma: no-cache instead
response->addHeader("Vary"s, "Origin");
// Generic OPTIONS response
if (!request->isCorsPreflightRequest())
{
response->addHeader("Allow", methods);
return response;
}
// CORS pre-flight response
std::string_view origin = drogon::utils::trim(request->getHeader("Origin"));
if (origin.empty())
{
response->setStatusCode(HttpStatusCode::k400BadRequest);
response->addHeader("X-Cors-Error",
"invalid empty Origin"); // diagnose help
return response;
}
// Check whether null origin is allowed (file://, sandboxed iframes, etc.)
if (drogon::utils::ci_equals(origin, "null") && !allowNullOrigin)
{
response->setStatusCode(HttpStatusCode::k403Forbidden);
response->addHeader("X-Cors-Error",
"null Origin not allowed"); // diagnose help
return response;
}
// Check whether the origin is allowed
if (originValidator && !originValidator(origin))
{
response->setStatusCode(HttpStatusCode::k403Forbidden);
response->addHeader("X-Cors-Error",
"origin not allowed"); // diagnose help
return response;
}
// Reflect the origin (acts like '*', that is forbidden when
// allowCredentials is true)
response->addHeader("Access-Control-Allow-Origin", std::string(origin));
response->addHeader("Access-Control-Allow-Methods", methods);
// Check requested method
// Policy: explicitly fail preflight with 40x + diagnostic header rather
// than silently returning allowed methods
auto acrMethod = drogon::utils::trim(
request->getHeader("Access-Control-Request-Method"));
if (acrMethod.empty())
{
response->setStatusCode(HttpStatusCode::k400BadRequest);
response->addHeader(
"X-Cors-Error",
"invalid empty Access-Control-Request-Method"); // diagnose help
return response;
}
const auto allowedMethods = drogon::utils::splitStringView(methods, ",");
if (std::find_if(allowedMethods.begin(),
allowedMethods.end(),
[&acrMethod](const std::string_view &method) {
return drogon::utils::ci_equals(method, acrMethod);
}) == allowedMethods.end())
{
response->setStatusCode(HttpStatusCode::k405MethodNotAllowed);
response->addHeader("Allow",
methods); // failing CORS pre-flight with 405 must
// also return the Allow header
response->addHeader("X-Cors-Error",
"method not allowed: "s.append(
acrMethod)); // diagnose help
return response;
}
// Allowed headers (intersection with requested ones on success, all allowed
// on error) Note: Browsers typically include only non-safelisted headers in
// Access-Control-Request-Headers We validate strictly against
// allowedHeaders Policy: explicitly fail preflight with 403 + diagnostic
// header rather than silently omitting forbidden CORS headers
auto requestedHeaders = drogon::utils::splitStringViewToSet(
request->getHeader("Access-Control-Request-Headers"), ",");
if (allowedHeaders.has_value())
{
auto &validHeaders = allowedHeaders.value();
if (requestedHeaders.empty()) // noisy, but helpful for diagnosis
requestedHeaders = {validHeaders.begin(), validHeaders.end()};
else
{
for (auto it = requestedHeaders.begin();
it != requestedHeaders.end();)
{
auto &reqHeader = *it;
if (std::find_if(validHeaders.begin(),
validHeaders.end(),
[&reqHeader](const std::string_view &header) {
return drogon::utils::ci_equals(
reqHeader,
drogon::utils::trim(header));
}) != validHeaders.end())
{
++it;
continue;
}
response->setStatusCode(
HttpStatusCode::k403Forbidden); // Forbidden header
response->addHeader("X-Cors-Error",
"disallowed header: "s.append(
reqHeader)); // diagnose help
// report all allowed headers to help diagnosing what's
// wrong
requestedHeaders = {validHeaders.begin(), validHeaders.end()};
break;
}
}
}
if (!requestedHeaders.empty())
response->addHeader("Access-Control-Allow-Headers",
drogon::utils::joinStringViews(requestedHeaders,
","));
if (response->statusCode() == HttpStatusCode::k403Forbidden)
return response;
// Allow credentials
if (allowCredentials)
response->addHeader("Access-Control-Allow-Credentials", "true");
// Chromium-based browsers require this header to allow Private Network
// Access requests
if (allowPNA &&
drogon::utils::ci_equals(request->getHeader(
"Access-Control-Request-Private-Network"),
"true"))
response->addHeader("Access-Control-Allow-Private-Network", "true");
// Set a max age only on success
if (maxAgeSeconds.has_value())
response->addHeader("Access-Control-Max-Age",
std::to_string(maxAgeSeconds.value()));
return response;
}
void HttpResponse::addCorsHeaders(
const HttpRequestPtr &request,
const std::set<std::string_view> &exposedHeaders,
const std::optional<bool> &allowCredentials)
{
if (!request || !request->isCorsRequest() ||
request->isCorsPreflightRequest())
return;
// add/set Origin to the Vary header (needed for cache proxies)
auto vary = drogon::utils::splitStringViewToSet(getHeader("Vary"), ",");
if (std::find_if(vary.begin(), vary.end(), [](const auto &val) {
return drogon::utils::ci_equals(val, "Origin");
}) == vary.end())
{
vary.insert("Origin");
addHeader("Vary", drogon::utils::joinStringViews(vary, ","));
}
// add _MISSING_ CORS header - do not overwrite existing one
if (headers().find("access-control-allow-origin") == headers().end())
addHeader("Access-Control-Allow-Origin",
std::string(
drogon::utils::trim(request->getHeader("Origin"))));
// set (or append) exposed headers
if (!exposedHeaders.empty())
{
auto exposed = drogon::utils::splitStringViewToSet(
getHeader("Access-Control-Expose-Headers"), ",");
bool changed = false;
for (auto &header : exposedHeaders)
{
if (std::find_if(exposed.begin(),
exposed.end(),
[&header](const auto &val) {
return drogon::utils::ci_equals(val, header);
}) != exposed.end())
continue;
exposed.insert(header);
changed = true;
}
if (changed)
addHeader("Access-Control-Expose-Headers",
drogon::utils::joinStringViews(exposed, ","));
}
if (!allowCredentials.has_value())
return;
if (allowCredentials.value())
addHeader("Access-Control-Allow-Credentials", "true");
else
removeHeader("Access-Control-Allow-Credentials");
}
void HttpResponseImpl::makeHeaderString(trantor::MsgBuffer &buffer)
{
buffer.ensureWritableBytes(128);
@@ -960,6 +1174,12 @@ void HttpResponseImpl::parseJson() const
bool HttpResponseImpl::shouldBeCompressed() const
{
// If the developer said "No" stop immediately.
if (!allowCompression_)
{
return false;
}
if (streamCallback_ || asyncStreamCallback_ || !sendfileName_.empty() ||
contentType() >= CT_APPLICATION_OCTET_STREAM ||
getBody().length() < 1024 ||
+6
View File
@@ -463,6 +463,12 @@ class DROGON_EXPORT HttpResponseImpl : public HttpResponse
}
private:
bool allowCompression_{true};
void setAllowCompression(bool allow) override;
bool allowCompression() const override;
void setBody(const char *body, size_t len) override
{
bodyPtr_ = std::make_shared<HttpMessageStringViewBody>(body, len);
+22 -6
View File
@@ -17,6 +17,8 @@
#include <trantor/utils/Logger.h>
#include <trantor/utils/MsgBuffer.h>
#include <algorithm>
#include <cerrno>
#include <cstdlib>
using namespace trantor;
using namespace drogon;
@@ -129,7 +131,16 @@ bool HttpResponseParser::parseResponse(MsgBuffer *buf)
// LOG_INFO << "content len=" << len;
if (!len.empty())
{
leftBodyLength_ = static_cast<size_t>(std::stoull(len));
try
{
leftBodyLength_ =
static_cast<size_t>(std::stoull(len));
}
catch (...)
{
// Malformed Content-Length from peer.
return false;
}
status_ = HttpResponseParseStatus::kExpectBody;
}
else
@@ -242,12 +253,17 @@ bool HttpResponseParser::parseResponse(MsgBuffer *buf)
const char *crlf = buf->findCRLF();
if (crlf)
{
// chunk length line
std::string len(buf->peek(), crlf - buf->peek());
char *end;
currentChunkLength_ = strtol(len.c_str(), &end, 16);
// LOG_TRACE << "chun length : " <<
// currentChunkLength_;
errno = 0;
char *end = nullptr;
unsigned long long parsed =
std::strtoull(len.c_str(), &end, 16);
if (errno == ERANGE || end == len.c_str() ||
(*end != '\0' && *end != ';'))
{
return false;
}
currentChunkLength_ = static_cast<size_t>(parsed);
if (currentChunkLength_ != 0)
{
status_ = HttpResponseParseStatus::kExpectChunkBody;
+19 -12
View File
@@ -130,15 +130,20 @@ void HttpServer::onConnection(const TcpConnectionPtr &conn)
else if (conn->disconnected())
{
LOG_TRACE << "conn disconnected!";
HttpConnectionLimit::instance().releaseConnection(conn);
auto requestParser = conn->getContext<HttpRequestParser>();
if (requestParser)
{
// NOTE: if tls handshake fails, `onConnection()` will only be
// called once with a broken conn. So we only call
// `releaseConnection()` for conn with context.
// Never call `conn->clearContext()` in other places
HttpConnectionLimit::instance().releaseConnection(conn);
if (requestParser->webSocketConn())
{
requestParser->webSocketConn()->onClose();
}
else if (requestParser->requestImpl()->isStreamMode())
else if (requestParser->requestImpl()->streamStatus() ==
ReqStreamStatus::Open)
{
requestParser->requestImpl()->streamError(
std::make_exception_ptr(
@@ -206,13 +211,9 @@ void HttpServer::onMessage(const TcpConnectionPtr &conn, MsgBuffer *buf)
statusCodeToString(code).data()));
}
buf->retrieveAll();
// NOTE: should we call conn->forceClose() instead?
// Calling shutdown() handles socket more elegantly.
// stop parser to ignore following illegal data from client
requestParser->stop();
conn->shutdown();
// We have to call clearContext() here in order to ignore following
// illegal data from client
conn->clearContext();
requestParser->reset();
return;
}
if (parseRes == 0)
@@ -576,12 +577,18 @@ void HttpServer::requestPassMiddlewares(const HttpRequestImplPtr &req,
template <typename Pack>
void HttpServer::requestPreHandling(const HttpRequestImplPtr &req, Pack &&pack)
{
// Handle CORS preflight request, except when custom handling is desired
if (req->method() == Options)
{
handleHttpOptions(req,
*pack.binderPtr->corsMethods_,
std::move(pack.callback));
return;
if (!req->attributes()->get<bool>("drogon.customCORShandling"))
{
handleHttpOptions(req,
*pack.binderPtr->corsMethods_,
std::move(pack.callback));
return;
}
req->attributes()->insert("drogon.corsMethods",
*pack.binderPtr->corsMethods_);
}
// pre-handling aop
+2 -25
View File
@@ -18,6 +18,7 @@
#include "HttpFileImpl.h"
#include <drogon/MultiPart.h>
#include <drogon/utils/Utilities.h>
#include "utils/ParsingUtils.h"
#include <drogon/config.h>
#include <algorithm>
#include <fcntl.h>
@@ -29,6 +30,7 @@
#endif
using namespace drogon;
using drogon::utils::parseLine;
const std::vector<HttpFile> &MultiPartParser::getFiles() const
{
@@ -87,31 +89,6 @@ int MultiPartParser::parse(const HttpRequestPtr &req)
return parse(req, contentType.data() + (pos + 9), pos2 - (pos + 9));
}
static std::pair<std::string_view, std::string_view> parseLine(
const char *begin,
const char *end)
{
auto p = begin;
while (p != end)
{
if (*p == ':')
{
if (p + 1 != end && *(p + 1) == ' ')
{
return std::make_pair(std::string_view(begin, p - begin),
std::string_view(p + 2, end - p - 2));
}
else
{
return std::make_pair(std::string_view(begin, p - begin),
std::string_view(p + 1, end - p - 1));
}
}
++p;
}
return std::make_pair(std::string_view(), std::string_view());
}
int MultiPartParser::parseEntity(const HttpRequestPtr &req,
const char *begin,
const char *end)
+4 -59
View File
@@ -14,41 +14,12 @@
#include "MultipartStreamParser.h"
#include <cassert>
#include "utils/ParsingUtils.h"
using namespace drogon;
static bool startsWith(const std::string_view &a, const std::string_view &b)
{
if (a.size() < b.size())
{
return false;
}
for (size_t i = 0; i < b.size(); i++)
{
if (a[i] != b[i])
{
return false;
}
}
return true;
}
static bool startsWithIgnoreCase(const std::string_view &a,
const std::string_view &b)
{
if (a.size() < b.size())
{
return false;
}
for (size_t i = 0; i < b.size(); i++)
{
if (::tolower(a[i]) != ::tolower(b[i]))
{
return false;
}
}
return true;
}
using drogon::utils::parseLine;
using drogon::utils::startsWith;
using drogon::utils::startsWithIgnoreCase;
MultipartStreamParser::MultipartStreamParser(const std::string &contentType)
{
@@ -86,32 +57,6 @@ MultipartStreamParser::MultipartStreamParser(const std::string &contentType)
crlfDashBoundary_ = crlf_ + dash_ + boundary_;
}
// TODO: same function in HttpRequestParser.cc
static std::pair<std::string_view, std::string_view> parseLine(
const char *begin,
const char *end)
{
auto p = begin;
while (p != end)
{
if (*p == ':')
{
if (p + 1 != end && *(p + 1) == ' ')
{
return std::make_pair(std::string_view(begin, p - begin),
std::string_view(p + 2, end - p - 2));
}
else
{
return std::make_pair(std::string_view(begin, p - begin),
std::string_view(p + 1, end - p - 1));
}
}
++p;
}
return std::make_pair(std::string_view(), std::string_view());
}
void drogon::MultipartStreamParser::parse(
const char *data,
size_t length,
+94 -29
View File
@@ -17,10 +17,49 @@
#include <dirent.h>
#include <dlfcn.h>
#include <fstream>
#include <sstream>
#include <sys/types.h>
#include <sys/wait.h>
#include <trantor/utils/Logger.h>
#include <unistd.h>
// Safe exec helper: runs a program with explicit argv, no shell involved.
// Returns the exit status, or -1 on fork/exec failure.
static int safeExec(const std::vector<std::string> &args)
{
if (args.empty())
return -1;
std::vector<char *> argv;
argv.reserve(args.size() + 1);
for (auto &a : args)
argv.push_back(const_cast<char *>(a.c_str()));
argv.push_back(nullptr);
pid_t pid = fork();
if (pid == -1)
{
perror("fork");
return -1;
}
if (pid == 0)
{
// Child: replace image with the target program.
execvp(argv[0], argv.data());
// execvp only returns on error.
perror("execvp");
_exit(127);
}
// Parent: wait for child.
int status = 0;
if (waitpid(pid, &status, 0) == -1)
{
perror("waitpid");
return -1;
}
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}
static void forEachFileIn(
const std::string &path,
const std::function<void(const std::string &, const struct stat &)> &cb)
@@ -153,22 +192,27 @@ void SharedLibManager::managerLibs()
else
{
// generate source code and compile it.
std::string cmd = "drogon_ctl create view ";
if (!outputPath_.empty())
{
cmd.append(filename).append(" -o ").append(
outputPath_);
}
else
{
cmd.append(filename).append(" -o ").append(
libPath);
}
const std::string &outDir =
!outputPath_.empty() ? outputPath_ : libPath;
std::vector<std::string> genArgs = {"drogon_ctl",
"create",
"view",
filename,
"-o",
outDir};
srcFile.append(".cc");
LOG_TRACE << cmd;
auto r = system(cmd.c_str());
// TODO: handle r
(void)(r);
LOG_TRACE << "drogon_ctl create view " << filename
<< " -o " << outDir;
auto r = safeExec(genArgs);
if (r != 0)
{
LOG_ERROR
<< "Failed to generate source code for "
<< filename;
dlStat.handle = oldHandle;
return;
}
dlStat.handle =
compileAndLoadLib(srcFile, oldHandle);
}
@@ -203,24 +247,45 @@ void *SharedLibManager::compileAndLoadLib(const std::string &sourceFile,
void *oldHld)
{
LOG_TRACE << "src:" << sourceFile;
std::string cmd = COMPILER_COMMAND;
cmd.append(" ")
.append(sourceFile)
.append(" ")
.append(COMPILATION_FLAGS)
.append(" ")
.append(INCLUDING_DIRS);
if (std::string(COMPILER_ID).find("Clang") != std::string::npos)
cmd.append(" -shared -fPIC -undefined dynamic_lookup -o ");
else
cmd.append(" -shared -fPIC --no-gnu-unique -o ");
auto pos = sourceFile.rfind('.');
auto soFile = sourceFile.substr(0, pos);
soFile.append(".so");
cmd.append(soFile);
LOG_TRACE << cmd;
if (system(cmd.c_str()) == 0)
// Build argv without invoking a shell so that metacharacters in
// sourceFile or soFile cannot be interpreted by /bin/sh.
std::vector<std::string> compileArgs;
compileArgs.push_back(COMPILER_COMMAND);
// COMPILATION_FLAGS and INCLUDING_DIRS are baked in at build time from
// trusted CMake variables; split them on whitespace into separate tokens.
auto splitIntoArgs = [&](const std::string &s) {
std::istringstream iss(s);
std::string token;
while (iss >> token)
compileArgs.push_back(token);
};
compileArgs.push_back(sourceFile);
splitIntoArgs(COMPILATION_FLAGS);
splitIntoArgs(INCLUDING_DIRS);
if (std::string(COMPILER_ID).find("Clang") != std::string::npos)
{
compileArgs.push_back("-shared");
compileArgs.push_back("-fPIC");
compileArgs.push_back("-undefined");
compileArgs.push_back("dynamic_lookup");
}
else
{
compileArgs.push_back("-shared");
compileArgs.push_back("-fPIC");
compileArgs.push_back("--no-gnu-unique");
}
compileArgs.push_back("-o");
compileArgs.push_back(soFile);
LOG_TRACE << COMPILER_COMMAND << " " << sourceFile << " ... -o " << soFile;
if (safeExec(compileArgs) == 0)
{
LOG_TRACE << "Compiled successfully:" << soFile;
return loadLib(soFile, oldHld);
+52 -11
View File
@@ -155,9 +155,24 @@ bool isInteger(std::string_view str)
bool isBase64(std::string_view str)
{
for (auto c : str)
if (!isBase64(c))
if (str.empty())
return false;
size_t padding = 0;
if (str.back() == '=')
padding++;
if (str.size() > 1 && str[str.size() - 2] == '=')
padding++;
for (size_t i = 0; i < str.size() - padding; ++i)
{
if (!isBase64(str[i]))
return false;
}
if (padding > 0 && (str.size() % 4 != 0))
return false;
return true;
}
@@ -1018,6 +1033,35 @@ std::string gzipDecompress(const char *data, const size_t ndata)
}
}
static int formatHttpDate(char *buf, size_t len, const trantor::Date &date)
{
static const char *const weekdays[] = {
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
static const char *const months[] = {"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec"};
struct tm tm = date.tmStruct();
return snprintf(buf,
len,
"%s, %02d %s %04d %02d:%02d:%02d GMT",
weekdays[tm.tm_wday],
tm.tm_mday,
months[tm.tm_mon],
tm.tm_year + 1900,
tm.tm_hour,
tm.tm_min,
tm.tm_sec);
}
char *getHttpFullDate(const trantor::Date &date)
{
static thread_local int64_t lastSecond = 0;
@@ -1029,9 +1073,7 @@ char *getHttpFullDate(const trantor::Date &date)
return lastTimeString;
}
lastSecond = nowSecond;
date.toCustomFormattedString("%a, %d %b %Y %H:%M:%S GMT",
lastTimeString,
sizeof(lastTimeString));
formatHttpDate(lastTimeString, sizeof(lastTimeString), date);
return lastTimeString;
}
@@ -1039,8 +1081,6 @@ void dateToCustomFormattedString(const std::string &fmtStr,
std::string &str,
const trantor::Date &date)
{
auto nowSecond =
date.microSecondsSinceEpoch() / trantor::Date::MICRO_SECONDS_PER_SEC;
struct tm tm_LValue = date.tmStruct();
std::stringstream Out;
Out.imbue(std::locale{"C"});
@@ -1051,7 +1091,7 @@ void dateToCustomFormattedString(const std::string &fmtStr,
const std::string &getHttpFullDateStr(const trantor::Date &date)
{
static thread_local int64_t lastSecond = 0;
static thread_local std::string lastTimeString(128, 0);
static thread_local std::string lastTimeString;
auto nowSecond =
date.microSecondsSinceEpoch() / trantor::Date::MICRO_SECONDS_PER_SEC;
if (nowSecond == lastSecond)
@@ -1059,9 +1099,10 @@ const std::string &getHttpFullDateStr(const trantor::Date &date)
return lastTimeString;
}
lastSecond = nowSecond;
dateToCustomFormattedString("%a, %d %b %Y %H:%M:%S GMT",
lastTimeString,
date);
lastTimeString.resize(128);
int n = formatHttpDate(lastTimeString.data(), lastTimeString.size(), date);
n = std::clamp(n, 0, static_cast<int>(lastTimeString.size() - 1));
lastTimeString.resize(static_cast<size_t>(n));
return lastTimeString;
}
+100
View File
@@ -0,0 +1,100 @@
/**
*
* @file ParsingUtils.h
* Shared parsing utilities for HTTP and multipart parsing
*
* Copyright 2024, Drogon. All rights reserved.
* https://github.com/drogonframework/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#pragma once
#include <cctype>
#include <string_view>
#include <utility>
namespace drogon
{
namespace utils
{
/**
* @brief Check if a string_view starts with another string_view
*/
inline bool startsWith(const std::string_view &a, const std::string_view &b)
{
if (a.size() < b.size())
{
return false;
}
for (size_t i = 0; i < b.size(); i++)
{
if (a[i] != b[i])
{
return false;
}
}
return true;
}
/**
* @brief Check if a string_view starts with another string_view
* (case-insensitive)
*/
inline bool startsWithIgnoreCase(const std::string_view &a,
const std::string_view &b)
{
if (a.size() < b.size())
{
return false;
}
for (size_t i = 0; i < b.size(); i++)
{
const auto lhs = std::tolower(static_cast<unsigned char>(a[i]));
const auto rhs = std::tolower(static_cast<unsigned char>(b[i]));
if (lhs != rhs)
{
return false;
}
}
return true;
}
/**
* @brief Parse a single HTTP header line into name and value
* @param begin Pointer to the start of the line
* @param end Pointer to the end of the line (not including CRLF)
* @return A pair of (header_name, header_value) string_views
*/
inline std::pair<std::string_view, std::string_view> parseLine(
const char *begin,
const char *end)
{
auto p = begin;
while (p != end)
{
if (*p == ':')
{
if (p + 1 != end && *(p + 1) == ' ')
{
return std::make_pair(std::string_view(begin, p - begin),
std::string_view(p + 2, end - p - 2));
}
else
{
return std::make_pair(std::string_view(begin, p - begin),
std::string_view(p + 1, end - p - 1));
}
}
++p;
}
return std::make_pair(std::string_view(), std::string_view());
}
} // namespace utils
} // namespace drogon
+1
View File
@@ -43,6 +43,7 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC" AND BUILD_SHARED_LIBS)
else()
set(UNITTEST_SOURCES ${UNITTEST_SOURCES} ../src/HttpFileImpl.cc
unittests/HttpFileTest.cc
unittests/HttpMethodTest.cc
unittests/WebsocketResponseTest.cc)
endif()
@@ -5,6 +5,7 @@
#include <string>
#include <iostream>
#include <fstream>
#include <filesystem>
using namespace drogon;
@@ -100,23 +101,46 @@ DROGON_TEST(RequestStreamTest)
LOG_INFO << "Test request stream";
std::string filePath = "./中文.txt";
std::ifstream file(filePath);
std::stringstream content;
REQUIRE(file.is_open());
content << file.rdbuf();
const auto uniqueSuffix = std::to_string(
std::chrono::steady_clock::now().time_since_epoch().count());
auto tempDir = std::make_shared<std::filesystem::path>(
std::filesystem::temp_directory_path() /
("request_stream_upload_test_" + uniqueSuffix));
std::filesystem::create_directories(*tempDir);
auto tempPath = std::make_shared<std::filesystem::path>(
*tempDir / std::filesystem::path(u8"中文.txt"));
tempPath->make_preferred();
{
std::ofstream out(*tempPath, std::ios::binary | std::ios::trunc);
REQUIRE(out.is_open());
out << "request-stream-upload-content\nline2\n";
}
req = HttpRequest::newFileUploadRequest({UploadFile{filePath}});
std::ifstream in(*tempPath, std::ios::binary);
REQUIRE(in.is_open());
std::stringstream ss;
ss << in.rdbuf();
const auto uploadContent = std::make_shared<std::string>(ss.str());
const auto uploadPathUtf8 = std::make_shared<std::string>([&tempPath]() {
auto u8Path = tempPath->u8string();
return std::string(reinterpret_cast<const char *>(u8Path.data()),
u8Path.size());
}());
req = HttpRequest::newFileUploadRequest({UploadFile{*uploadPathUtf8}});
req->setPath("/stream_upload_echo");
req->setMethod(Post);
client->sendRequest(req,
[TEST_CTX,
content = content.str()](ReqResult r,
const HttpResponsePtr &resp) {
CHECK(r == ReqResult::Ok);
CHECK(resp->statusCode() == k200OK);
CHECK(resp->body() == content);
});
client->sendRequest(
req,
[TEST_CTX, tempPath, tempDir, uploadPathUtf8, content = uploadContent](
ReqResult r, const HttpResponsePtr &resp) {
CHECK(r == ReqResult::Ok);
CHECK(resp->statusCode() == k200OK);
CHECK(resp->body() == *content);
std::error_code ec;
std::filesystem::remove(*tempPath, ec);
std::filesystem::remove(*tempDir, ec);
});
checkStreamRequest(TEST_CTX,
client->getLoop(),
@@ -728,6 +728,23 @@ void doTest(const HttpClientPtr &client, std::shared_ptr<test::Case> TEST_CTX)
CHECK((*json)["P2"] == "test");
});
// Test file upload from memory
auto hello = std::make_shared<std::string>("hello world!");
UploadFile memfile(hello->data(),
hello->length(),
"hello_world.txt",
"hellofile",
ContentType::CT_TEXT_PLAIN);
req = HttpRequest::newFileUploadRequest({memfile});
req->setPath("/api/attachment/uploadMemory");
client->sendRequest(req,
[req, TEST_CTX, hello](ReqResult result,
const HttpResponsePtr &resp) {
REQUIRE(result == ReqResult::Ok);
REQUIRE(resp->contentType() == CT_TEXT_PLAIN);
CHECK(resp->getBody() == *hello);
});
// Test newFileResponse
req = HttpRequest::newHttpRequest();
req->setPath("/RangeTestController/");
@@ -103,6 +103,32 @@ void Attachment::uploadImage(
callback(resp);
}
void Attachment::uploadMemory(
const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback)
{
MultiPartParser fileUpload;
if (fileUpload.parse(req) == 0 && fileUpload.getFiles().size() == 1)
{
auto &file = fileUpload.getFiles()[0];
if (file.getItemName() == "hellofile")
{
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(HttpStatusCode::k200OK);
resp->setContentTypeCode(ContentType::CT_TEXT_PLAIN);
std::string hello = std::string(file.fileData(), file.fileLength());
resp->setBody(std::move(hello));
callback(resp);
return;
}
}
LOG_DEBUG << "upload text from memory error!";
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(HttpStatusCode::k400BadRequest);
callback(resp);
}
void Attachment::download(
const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback)
@@ -12,6 +12,7 @@ class Attachment : public drogon::HttpController<Attachment>
METHOD_ADD(Attachment::get, "", Get); // Path is '/api/attachment'
METHOD_ADD(Attachment::upload, "/upload", Post);
METHOD_ADD(Attachment::uploadImage, "/uploadImage", Post);
METHOD_ADD(Attachment::uploadMemory, "/uploadMemory", Post);
METHOD_ADD(Attachment::download, "/download", Get);
METHOD_LIST_END
// your declaration of processing function maybe like this:
@@ -21,6 +22,8 @@ class Attachment : public drogon::HttpController<Attachment>
std::function<void(const HttpResponsePtr &)> &&callback);
void uploadImage(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback);
void uploadMemory(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback);
void download(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback);
};
@@ -9,6 +9,7 @@ DROGON_TEST(Base64)
auto decoded = drogon::utils::base64Decode(encoded);
CHECK(encoded == "ZHJvZ29uIGZyYW1ld29yaw==");
CHECK(decoded == in);
CHECK(drogon::utils::isBase64(encoded));
SUBSECTION(InvalidChars)
{
@@ -31,6 +32,7 @@ DROGON_TEST(Base64)
auto decoded = drogon::utils::base64Decode(encoded);
CHECK(encoded == "ZHJvZ29uIGZyYW1ld29yaw");
CHECK(decoded == in);
CHECK(drogon::utils::isBase64(encoded));
}
SUBSECTION(LongString)
@@ -46,6 +48,9 @@ DROGON_TEST(Base64)
auto encoded = drogon::utils::base64Encode(in);
auto decoded = drogon::utils::base64Decode(encoded);
CHECK(decoded == in);
CHECK(out == encoded);
CHECK(drogon::utils::isBase64(out));
CHECK(drogon::utils::isBase64(encoded));
}
SUBSECTION(URLSafe)
@@ -55,6 +60,7 @@ DROGON_TEST(Base64)
auto decoded = drogon::utils::base64Decode(encoded);
CHECK(encoded == "ZHJvZ29uIGZyYW1ld29yaw==");
CHECK(decoded == in);
CHECK(drogon::utils::isBase64(encoded));
}
SUBSECTION(UnpaddedURLSafe)
@@ -64,6 +70,7 @@ DROGON_TEST(Base64)
auto decoded = drogon::utils::base64Decode(encoded);
CHECK(encoded == "ZHJvZ29uIGZyYW1ld29yaw");
CHECK(decoded == in);
CHECK(drogon::utils::isBase64(encoded));
}
SUBSECTION(LongURLSafe)
@@ -77,5 +84,24 @@ DROGON_TEST(Base64)
auto encoded = drogon::utils::base64Encode(in, true);
auto decoded = drogon::utils::base64Decode(encoded);
CHECK(decoded == in);
CHECK(drogon::utils::isBase64(encoded));
}
SUBSECTION(emptyString)
{
auto encoded = "";
CHECK(!drogon::utils::isBase64(encoded));
}
SUBSECTION(size1Padding)
{
auto encoded = "ZHJvZ29uIGZyYW1ld29=";
CHECK(drogon::utils::isBase64(encoded));
}
SUBSECTION(size1PaddingNotModulo4)
{
auto encoded = "ZHJvZ29uIGZyYW1ld29ya=";
CHECK(!drogon::utils::isBase64(encoded));
}
}
@@ -66,3 +66,195 @@ DROGON_TEST(ResquestSetCustomContentTypeString)
req->setContentTypeString("thisdoesnotexist/unknown");
CHECK(req->getContentType() == CT_CUSTOM);
}
DROGON_TEST(HttpOptionsHeadersResponse)
{
auto req = HttpRequest::newHttpRequest();
auto resp = HttpResponse::newOptionsResponse(req);
CHECK(!resp);
req->setMethod(HttpMethod::Options);
resp = HttpResponse::newOptionsResponse(req);
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Vary") == "Origin");
CHECK(resp->getHeader("Allow") == "OPTIONS");
CHECK(resp->getHeader("Access-Control-Allow-Origin") == "");
CHECK(resp->getHeader("Access-Control-Allow-Methods") == "");
req->attributes()->insert("drogon.corsMethods",
std::string("GET, POST, OPTIONS"));
resp = HttpResponse::newOptionsResponse(req);
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Vary") == "Origin");
CHECK(resp->getHeader("Allow") == "GET, POST, OPTIONS");
CHECK(resp->getHeader("Access-Control-Allow-Origin") == "");
CHECK(resp->getHeader("Access-Control-Allow-Methods") == "");
req->addHeader("Origin", "http://somepage");
resp = HttpResponse::newOptionsResponse(req);
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Vary") == "Origin");
CHECK(resp->getHeader("Allow") == "GET, POST, OPTIONS");
CHECK(resp->getHeader("Access-Control-Allow-Origin") == "");
CHECK(resp->getHeader("Access-Control-Allow-Methods") == "");
}
DROGON_TEST(HttpCorsHeadersResponse)
{
auto req = HttpRequest::newHttpRequest();
req->addHeader("Origin", "");
req->addHeader("Access-Control-Request-Method", "OPTIONS");
auto resp = HttpResponse::newOptionsResponse(req);
CHECK(!resp);
// empty origin -> error
req->setMethod(HttpMethod::Options);
resp = HttpResponse::newOptionsResponse(req);
CHECK(resp->getStatusCode() == HttpStatusCode::k400BadRequest);
// null origin -> check if allowed or not
req->addHeader("Origin", "null");
resp = HttpResponse::newOptionsResponse(req);
CHECK(resp->getStatusCode() == HttpStatusCode::k403Forbidden);
resp = HttpResponse::newOptionsResponse(req, {}, true);
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Access-Control-Allow-Origin") == "null");
// normal origin but no requested method -> error
req->addHeader("Origin", "http://somepage");
req->addHeader("Access-Control-Request-Method", "");
resp = HttpResponse::newOptionsResponse(req, {}, true);
CHECK(resp->getStatusCode() == HttpStatusCode::k400BadRequest);
// valid CORS preflight request
req->addHeader("Access-Control-Request-Method", "OPTIONS");
resp = HttpResponse::newOptionsResponse(req);
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Vary") == "Origin");
CHECK(resp->getHeader("Allow") == "");
CHECK(resp->getHeader("Access-Control-Allow-Origin") == "http://somepage");
CHECK(resp->getHeader("Access-Control-Allow-Methods") == "OPTIONS");
// origin validator
resp = HttpResponse::newOptionsResponse(req, [](std::string_view origin) {
return origin == "http://somepage";
});
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
resp = HttpResponse::newOptionsResponse(req, [](std::string_view origin) {
return origin != "http://somepage";
});
CHECK(resp->getStatusCode() == HttpStatusCode::k403Forbidden);
// unallowed method
req->addHeader("Access-Control-Request-Method", "PUT");
req->attributes()->insert("drogon.corsMethods",
std::string("GET,POST,OPTIONS"));
resp = HttpResponse::newOptionsResponse(req);
CHECK(resp->getStatusCode() == HttpStatusCode::k405MethodNotAllowed);
CHECK(resp->getHeader("Allow") == "GET,POST,OPTIONS");
CHECK(resp->getHeader("Access-Control-Allow-Methods") ==
"GET,POST,OPTIONS");
// allowed method
req->addHeader("Access-Control-Request-Method", "GET");
resp = HttpResponse::newOptionsResponse(req);
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Allow") == "");
CHECK(resp->getHeader("Access-Control-Allow-Origin") == "http://somepage");
CHECK(resp->getHeader("Access-Control-Allow-Methods") ==
"GET,POST,OPTIONS");
CHECK(resp->getHeader("Access-Control-Allow-Credentials") == "");
CHECK(resp->getHeader("Access-Control-Allow-Private-Network") == "");
CHECK(resp->getHeader("Access-Control-Max-Age") == "");
// no restriction on requested headers
req->addHeader("Access-Control-Request-Headers", "X-Foo, X-Bar");
resp = HttpResponse::newOptionsResponse(req);
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Access-Control-Allow-Headers") == "X-Bar,X-Foo");
// unallowed header
resp = HttpResponse::newOptionsResponse(req, {"X-Foo"});
CHECK(resp->getStatusCode() == HttpStatusCode::k403Forbidden);
CHECK(resp->getHeader("Access-Control-Allow-Headers") == "X-Foo");
// all requested headers allowed
resp = HttpResponse::newOptionsResponse(req, {"X-Foo", "X-Bar"});
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Access-Control-Allow-Headers") == "X-Bar,X-Foo");
// allow credentials
resp = HttpResponse::newOptionsResponse(req, nullptr, false, true);
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Access-Control-Allow-Credentials") == "true");
// private network access
req->addHeader("Access-Control-Request-Private-Network", "true");
resp = HttpResponse::newOptionsResponse(req, nullptr, false, false, false);
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Access-Control-Allow-Private-Network") == "");
resp = HttpResponse::newOptionsResponse(req, nullptr, false, false, true);
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Access-Control-Allow-Private-Network") == "true");
// CORS max age
resp = HttpResponse::newOptionsResponse(
req, nullptr, false, false, false, 600);
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Access-Control-Max-Age") == "600");
}
DROGON_TEST(AddHttpCorsHeaders)
{
using namespace std::literals;
// no Origin -> do nothing
auto req = HttpRequest::newHttpRequest();
req->setMethod(Get);
auto resp = HttpResponse::newHttpResponse();
resp->addCorsHeaders(req, {"X-Foo"}, true);
CHECK(resp->headers().empty());
// with Origin -> Allow-Origin + Vary (not overwritten) + Expose-Headers
req->addHeader("Origin", "http://somepage");
resp->addHeader("Vary", "X-SomeHeader");
resp->addCorsHeaders(req, {"X-Foo"});
CHECK(resp->getHeader("Vary") == "Origin,X-SomeHeader");
CHECK(resp->getHeader("Access-Control-Allow-Origin") == "http://somepage");
CHECK(resp->getHeader("Access-Control-Expose-Headers") == "X-Foo");
// add a new exposed header
resp->addCorsHeaders(req, {"X-Bar"});
CHECK(resp->getHeader("Access-Control-Expose-Headers") == "X-Bar,X-Foo");
// no duplicate Origin in Vary
CHECK(resp->getHeader("Vary") == "Origin,X-SomeHeader");
// check credentials (true/false/unchanged)
resp->addCorsHeaders(req, {}, true);
CHECK(resp->getHeader("Access-Control-Expose-Headers") == "X-Bar,X-Foo");
CHECK(resp->getHeader("Access-Control-Allow-Credentials") == "true");
resp->addCorsHeaders(req, {}, false);
CHECK(resp->getHeader("Access-Control-Allow-Credentials") == "");
resp->addCorsHeaders(req, {}, true);
resp->addCorsHeaders(req);
CHECK(resp->getHeader("Access-Control-Allow-Credentials") == "true");
}
DROGON_TEST(ClearHeaders)
{
auto req = HttpRequest::newHttpRequest();
// set a custom path to ensure it is not cleared
req->setPath("/api/test");
req->addHeader("X-Test", "value");
req->addHeader("Authorization", "Bearer token");
CHECK(req->headers().size() == 2);
CHECK(req->getHeader("X-Test") == "value");
CHECK(req->getHeader("Authorization") == "Bearer token");
req->clearHeaders();
CHECK(req->headers().empty());
// verify path unchanged
CHECK(req->path() == "/api/test");
}
@@ -0,0 +1,120 @@
#include <drogon/drogon_test.h>
#include <drogon/HttpTypes.h>
#include <trantor/utils/MsgBuffer.h>
#include "../../lib/src/HttpRequestImpl.h"
using namespace drogon;
// Helper: parse a method string through HttpRequestImpl::setMethod
static std::pair<bool, HttpMethod> parseMethod(const std::string &str)
{
HttpRequestImpl req(nullptr);
bool ok = req.setMethod(str.data(), str.data() + str.size());
return {ok, req.method()};
}
DROGON_TEST(StandardHttpMethods)
{
auto [ok, m] = parseMethod("GET");
CHECK(ok);
CHECK(m == Get);
std::tie(ok, m) = parseMethod("POST");
CHECK(ok);
CHECK(m == Post);
std::tie(ok, m) = parseMethod("PUT");
CHECK(ok);
CHECK(m == Put);
std::tie(ok, m) = parseMethod("DELETE");
CHECK(ok);
CHECK(m == Delete);
std::tie(ok, m) = parseMethod("HEAD");
CHECK(ok);
CHECK(m == Head);
std::tie(ok, m) = parseMethod("OPTIONS");
CHECK(ok);
CHECK(m == Options);
std::tie(ok, m) = parseMethod("PATCH");
CHECK(ok);
CHECK(m == Patch);
}
DROGON_TEST(WebDavMethods)
{
auto [ok, m] = parseMethod("PROPFIND");
CHECK(ok);
CHECK(m == Propfind);
std::tie(ok, m) = parseMethod("MKCOL");
CHECK(ok);
CHECK(m == Mkcol);
std::tie(ok, m) = parseMethod("COPY");
CHECK(ok);
CHECK(m == Copy);
std::tie(ok, m) = parseMethod("MOVE");
CHECK(ok);
CHECK(m == Move);
}
DROGON_TEST(WebDavMethodStrings)
{
CHECK(to_string_view(Propfind) == "PROPFIND");
CHECK(to_string_view(Mkcol) == "MKCOL");
CHECK(to_string_view(Copy) == "COPY");
CHECK(to_string_view(Move) == "MOVE");
}
// Helper: serialize a request and return the first line (method + path)
static std::string serializeMethod(HttpMethod method)
{
HttpRequestImpl req(nullptr);
req.setMethod(method);
req.setPath("/test");
trantor::MsgBuffer buf;
req.appendToBuffer(&buf);
std::string result(buf.peek(), buf.readableBytes());
// Return just up to the first space after the method
auto pos = result.find(' ');
return result.substr(0, pos);
}
DROGON_TEST(MethodSerialization)
{
CHECK(serializeMethod(Get) == "GET");
CHECK(serializeMethod(Post) == "POST");
CHECK(serializeMethod(Put) == "PUT");
CHECK(serializeMethod(Delete) == "DELETE");
CHECK(serializeMethod(Head) == "HEAD");
CHECK(serializeMethod(Options) == "OPTIONS");
CHECK(serializeMethod(Patch) == "PATCH");
CHECK(serializeMethod(Propfind) == "PROPFIND");
CHECK(serializeMethod(Mkcol) == "MKCOL");
CHECK(serializeMethod(Copy) == "COPY");
CHECK(serializeMethod(Move) == "MOVE");
}
DROGON_TEST(InvalidMethodsRejected)
{
auto [ok, m] = parseMethod("INVALID");
CHECK(!ok);
CHECK(m == Invalid);
std::tie(ok, m) = parseMethod("LOCK");
CHECK(!ok);
CHECK(m == Invalid);
std::tie(ok, m) = parseMethod("");
CHECK(!ok);
CHECK(m == Invalid);
std::tie(ok, m) = parseMethod("G");
CHECK(!ok);
CHECK(m == Invalid);
}
@@ -67,6 +67,69 @@ struct Filter
std::string value;
};
/**
* @brief Represents a SQL JOIN clause.
*/
enum class JoinType
{
InnerJoin,
LeftJoin,
RightJoin,
FullJoin
};
inline std::string to_join_string(JoinType type)
{
switch (type)
{
case JoinType::InnerJoin:
return "INNER JOIN";
case JoinType::LeftJoin:
return "LEFT JOIN";
case JoinType::RightJoin:
return "RIGHT JOIN";
case JoinType::FullJoin:
return "FULL JOIN";
}
// Should never reach here
return "INNER JOIN";
}
struct JoinClause
{
JoinType type;
std::string table;
std::string onLeft; // e.g. "users.id"
std::string onRight; // e.g. "posts.user_id"
};
/**
* @brief Validate that a string is a safe SQL identifier.
*
* Only allows alphanumeric characters, underscores, and dots
* (for table.column notation). This prevents SQL injection when
* building JOIN clauses from user-provided identifiers.
*
* @param identifier The identifier to validate.
* @return true if the identifier is safe to use in SQL.
*/
inline bool isValidSqlIdentifier(const std::string &identifier)
{
if (identifier.empty())
{
return false;
}
for (auto c : identifier)
{
if (!std::isalnum(static_cast<unsigned char>(c)) && c != '_' &&
c != '.')
{
return false;
}
}
return true;
}
// Forward declaration to be a friend
template <typename T, bool SelectAll, bool Single = false>
class TransformBuilder;
@@ -87,6 +150,7 @@ class BaseBuilder
std::string from_;
std::string columns_;
std::vector<Filter> filters_;
std::vector<JoinClause> joins_;
std::optional<std::uint64_t> limit_;
std::optional<std::uint64_t> offset_;
// The order is important; use vector<pair> instead of unordered_map and
@@ -122,6 +186,11 @@ class BaseBuilder
};
std::string sql = "select " + columns_ + " from " + from_;
for (const auto &join : joins_)
{
sql += " " + to_join_string(join.type) + " " + join.table + " ON " +
join.onLeft + " = " + join.onRight;
}
if (!filters_.empty())
{
sql += " where " + filters_[0].column + " " +
@@ -211,6 +211,54 @@ class CoroMapper : public Mapper<T>
return *this;
}
/**
* @brief Add an INNER JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of ON (e.g. "users.id").
* @param onRight The right side of ON (e.g. "posts.user_id").
* @return CoroMapper<T>& The CoroMapper itself.
*/
CoroMapper<T> &innerJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
Mapper<T>::innerJoin(table, onLeft, onRight);
return *this;
}
/**
* @brief Add a LEFT JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of ON (e.g. "users.id").
* @param onRight The right side of ON (e.g. "posts.user_id").
* @return CoroMapper<T>& The CoroMapper itself.
*/
CoroMapper<T> &leftJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
Mapper<T>::leftJoin(table, onLeft, onRight);
return *this;
}
/**
* @brief Add a RIGHT JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of ON (e.g. "users.id").
* @param onRight The right side of ON (e.g. "posts.user_id").
* @return CoroMapper<T>& The CoroMapper itself.
*/
CoroMapper<T> &rightJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
Mapper<T>::rightJoin(table, onLeft, onRight);
return *this;
}
// Read api for coroutines
inline internal::MapperAwaiter<std::vector<T>> findAll()
@@ -225,6 +273,7 @@ class CoroMapper : public Mapper<T>
ExceptPtrCallback &&errCallback) {
std::string sql = "select count(*) from ";
sql += T::tableName;
sql += this->joinString_;
if (criteria)
{
sql += " where ";
@@ -250,6 +299,7 @@ class CoroMapper : public Mapper<T>
ExceptPtrCallback &&errCallback) {
std::string sql = "select * from ";
sql += T::tableName;
sql += this->joinString_;
bool hasParameters = false;
if (criteria)
{
@@ -311,6 +361,7 @@ class CoroMapper : public Mapper<T>
ExceptPtrCallback &&errCallback) {
std::string sql = "select * from ";
sql += T::tableName;
sql += this->joinString_;
bool hasParameters = false;
if (criteria)
{
+41 -6
View File
@@ -43,6 +43,15 @@ using ExceptionCallback = std::function<void(const DrogonDbException &)>;
class Transaction;
class DbClient;
/// Transaction locking mode.
enum class TransactionType
{
Deferred, ///< BEGIN — lock acquired on first write (default)
Immediate, ///< BEGIN IMMEDIATE — write lock acquired upfront (SQLite only)
Exclusive, ///< BEGIN EXCLUSIVE — exclusive lock acquired upfront (SQLite
///< only)
};
namespace internal
{
#ifdef __cpp_impl_coroutine
@@ -73,7 +82,10 @@ struct [[nodiscard]] SqlAwaiter : public CallbackAwaiter<Result>
struct [[nodiscard]] TransactionAwaiter
: public CallbackAwaiter<std::shared_ptr<Transaction> >
{
explicit TransactionAwaiter(DbClient *client) : client_(client)
explicit TransactionAwaiter(
DbClient *client,
TransactionType transType = TransactionType::Deferred)
: client_(client), transType_(transType)
{
}
@@ -81,6 +93,7 @@ struct [[nodiscard]] TransactionAwaiter
private:
DbClient *client_;
TransactionType transType_;
};
#endif
@@ -269,7 +282,16 @@ class DROGON_EXPORT DbClient : public trantor::NonCopyable
*/
virtual std::shared_ptr<Transaction> newTransaction(
const std::function<void(bool)> &commitCallback =
std::function<void(bool)>()) noexcept(false) = 0;
std::function<void(bool)>(),
TransactionType transType =
TransactionType::Deferred) noexcept(false) = 0;
/// Convenience overload: create a transaction with a specific locking mode.
std::shared_ptr<Transaction> newTransaction(
TransactionType transType) noexcept(false)
{
return newTransaction(std::function<void(bool)>(), transType);
}
/// Create a transaction object in asynchronous mode.
/**
@@ -278,12 +300,24 @@ class DROGON_EXPORT DbClient : public trantor::NonCopyable
*/
virtual void newTransactionAsync(
const std::function<void(const std::shared_ptr<Transaction> &)>
&callback) = 0;
&callback,
TransactionType transType = TransactionType::Deferred) = 0;
/// Convenience overload: create an async transaction with a specific
/// locking mode, with transType as the first argument.
void newTransactionAsync(
TransactionType transType,
const std::function<void(const std::shared_ptr<Transaction> &)>
&callback)
{
newTransactionAsync(callback, transType);
}
#ifdef __cpp_impl_coroutine
orm::internal::TransactionAwaiter newTransactionCoro()
orm::internal::TransactionAwaiter newTransactionCoro(
TransactionType transType = TransactionType::Deferred)
{
return orm::internal::TransactionAwaiter(this);
return orm::internal::TransactionAwaiter(this, transType);
}
#endif
@@ -408,7 +442,8 @@ inline void internal::TransactionAwaiter::await_suspend(
else
setValue(transaction);
handle.resume();
});
},
transType_);
}
#endif
@@ -157,6 +157,90 @@ class FilterBuilder : public TransformBuilder<T, SelectAll, false>
this->filters_.push_back({column, CompareOperator::Like, pattern});
return *this;
}
/**
* @brief Add an INNER JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of the ON condition (e.g. "users.id").
* @param onRight The right side of the ON condition (e.g.
* "posts.user_id").
*
* @return FilterBuilder& The FilterBuilder itself.
*/
inline FilterBuilder &innerJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
assert(isValidSqlIdentifier(table));
assert(isValidSqlIdentifier(onLeft));
assert(isValidSqlIdentifier(onRight));
this->joins_.push_back({JoinType::InnerJoin, table, onLeft, onRight});
return *this;
}
/**
* @brief Add a LEFT JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of the ON condition (e.g. "users.id").
* @param onRight The right side of the ON condition (e.g.
* "posts.user_id").
*
* @return FilterBuilder& The FilterBuilder itself.
*/
inline FilterBuilder &leftJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
assert(isValidSqlIdentifier(table));
assert(isValidSqlIdentifier(onLeft));
assert(isValidSqlIdentifier(onRight));
this->joins_.push_back({JoinType::LeftJoin, table, onLeft, onRight});
return *this;
}
/**
* @brief Add a RIGHT JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of the ON condition (e.g. "users.id").
* @param onRight The right side of the ON condition (e.g.
* "posts.user_id").
*
* @return FilterBuilder& The FilterBuilder itself.
*/
inline FilterBuilder &rightJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
assert(isValidSqlIdentifier(table));
assert(isValidSqlIdentifier(onLeft));
assert(isValidSqlIdentifier(onRight));
this->joins_.push_back({JoinType::RightJoin, table, onLeft, onRight});
return *this;
}
/**
* @brief Add a FULL JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of the ON condition (e.g. "users.id").
* @param onRight The right side of the ON condition (e.g.
* "posts.user_id").
*
* @return FilterBuilder& The FilterBuilder itself.
*/
inline FilterBuilder &fullJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
assert(isValidSqlIdentifier(table));
assert(isValidSqlIdentifier(onLeft));
assert(isValidSqlIdentifier(onRight));
this->joins_.push_back({JoinType::FullJoin, table, onLeft, onRight});
return *this;
}
};
} // namespace orm
} // namespace drogon
+90 -3
View File
@@ -14,6 +14,7 @@
#pragma once
#include <drogon/orm/Criteria.h>
#include <drogon/orm/BaseBuilder.h>
#include <drogon/orm/DbClient.h>
#include <drogon/utils/Utilities.h>
#include <string>
@@ -178,6 +179,78 @@ class Mapper
*/
Mapper<T> &forUpdate();
/**
* @brief Add an INNER JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of ON (e.g. "users.id").
* @param onRight The right side of ON (e.g. "posts.user_id").
* @return Mapper<T>& The Mapper itself.
*/
Mapper<T> &innerJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
assert(isValidSqlIdentifier(table));
assert(isValidSqlIdentifier(onLeft));
assert(isValidSqlIdentifier(onRight));
joinString_ += " INNER JOIN ";
joinString_ += table;
joinString_ += " ON ";
joinString_ += onLeft;
joinString_ += " = ";
joinString_ += onRight;
return *this;
}
/**
* @brief Add a LEFT JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of ON (e.g. "users.id").
* @param onRight The right side of ON (e.g. "posts.user_id").
* @return Mapper<T>& The Mapper itself.
*/
Mapper<T> &leftJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
assert(isValidSqlIdentifier(table));
assert(isValidSqlIdentifier(onLeft));
assert(isValidSqlIdentifier(onRight));
joinString_ += " LEFT JOIN ";
joinString_ += table;
joinString_ += " ON ";
joinString_ += onLeft;
joinString_ += " = ";
joinString_ += onRight;
return *this;
}
/**
* @brief Add a RIGHT JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of ON (e.g. "users.id").
* @param onRight The right side of ON (e.g. "posts.user_id").
* @return Mapper<T>& The Mapper itself.
*/
Mapper<T> &rightJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
assert(isValidSqlIdentifier(table));
assert(isValidSqlIdentifier(onLeft));
assert(isValidSqlIdentifier(onRight));
joinString_ += " RIGHT JOIN ";
joinString_ += table;
joinString_ += " ON ";
joinString_ += onLeft;
joinString_ += " = ";
joinString_ += onRight;
return *this;
}
using SingleRowCallback = std::function<void(T)>;
using MultipleRowsCallback = std::function<void(std::vector<T>)>;
using CountCallback = std::function<void(const size_t)>;
@@ -719,6 +792,7 @@ class Mapper
size_t limit_{0};
size_t offset_{0};
std::string orderByString_;
std::string joinString_;
bool forUpdate_{false};
void clear()
@@ -726,6 +800,7 @@ class Mapper
limit_ = 0;
offset_ = 0;
orderByString_.clear();
joinString_.clear();
forUpdate_ = false;
}
@@ -792,6 +867,7 @@ inline T Mapper<T>::findOne(const Criteria &criteria) noexcept(false)
{
std::string sql = "select * from ";
sql += T::tableName;
sql += joinString_;
bool hasParameters = false;
if (criteria)
{
@@ -849,6 +925,7 @@ inline void Mapper<T>::findOne(const Criteria &criteria,
{
std::string sql = "select * from ";
sql += T::tableName;
sql += joinString_;
bool hasParameters = false;
if (criteria)
{
@@ -904,6 +981,7 @@ inline std::future<T> Mapper<T>::findFutureOne(
{
std::string sql = "select * from ";
sql += T::tableName;
sql += joinString_;
bool hasParameters = false;
if (criteria)
{
@@ -964,6 +1042,7 @@ inline std::vector<T> Mapper<T>::findBy(const Criteria &criteria) noexcept(
{
std::string sql = "select * from ";
sql += T::tableName;
sql += joinString_;
bool hasParameters = false;
if (criteria)
{
@@ -1003,9 +1082,10 @@ inline std::vector<T> Mapper<T>::findBy(const Criteria &criteria) noexcept(
binder.exec(); // exec may be throw exception;
}
std::vector<T> ret;
ret.reserve(r.size());
for (auto const &row : r)
{
ret.push_back(T(row));
ret.emplace_back(row);
}
return ret;
}
@@ -1017,6 +1097,7 @@ inline void Mapper<T>::findBy(const Criteria &criteria,
{
std::string sql = "select * from ";
sql += T::tableName;
sql += joinString_;
bool hasParameters = false;
if (criteria)
{
@@ -1051,6 +1132,7 @@ inline void Mapper<T>::findBy(const Criteria &criteria,
clear();
binder >> [rcb](const Result &r) {
std::vector<T> ret;
ret.reserve(r.size());
for (auto const &row : r)
{
ret.emplace_back(row);
@@ -1066,6 +1148,7 @@ inline std::future<std::vector<T>> Mapper<T>::findFutureBy(
{
std::string sql = "select * from ";
sql += T::tableName;
sql += joinString_;
bool hasParameters = false;
if (criteria)
{
@@ -1102,11 +1185,12 @@ inline std::future<std::vector<T>> Mapper<T>::findFutureBy(
std::make_shared<std::promise<std::vector<T>>>();
binder >> [prom](const Result &r) {
std::vector<T> ret;
ret.reserve(r.size());
for (auto const &row : r)
{
ret.push_back(T(row));
ret.emplace_back(row);
}
prom->set_value(ret);
prom->set_value(std::move(ret));
};
binder >> [prom](const std::exception_ptr &e) { prom->set_exception(e); };
binder.exec();
@@ -1137,6 +1221,7 @@ inline size_t Mapper<T>::count(const Criteria &criteria) noexcept(false)
{
std::string sql = "select count(*) from ";
sql += T::tableName;
sql += joinString_;
if (criteria)
{
sql += " where ";
@@ -1164,6 +1249,7 @@ inline void Mapper<T>::count(const Criteria &criteria,
{
std::string sql = "select count(*) from ";
sql += T::tableName;
sql += joinString_;
if (criteria)
{
sql += " where ";
@@ -1187,6 +1273,7 @@ inline std::future<size_t> Mapper<T>::countFuture(
{
std::string sql = "select count(*) from ";
sql += T::tableName;
sql += joinString_;
if (criteria)
{
sql += " where ";
+26 -13
View File
@@ -197,7 +197,8 @@ void DbClientImpl::execSql(
}
void DbClientImpl::newTransactionAsync(
const std::function<void(const std::shared_ptr<Transaction> &)> &callback)
const std::function<void(const std::shared_ptr<Transaction> &)> &callback,
TransactionType transType)
{
DbConnectionPtr conn;
{
@@ -231,7 +232,7 @@ void DbClientImpl::newTransactionAsync(
iter != transCallbacks_.end();
++iter)
{
if (cbPtr == *iter)
if (cbPtr == iter->first)
{
transCallbacks_.erase(iter);
break;
@@ -251,24 +252,29 @@ void DbClientImpl::newTransactionAsync(
(*newCallbackPtr) = callbackPtr;
timeoutFlagPtr->runTimer();
}
transCallbacks_.push_back(callbackPtr);
transCallbacks_.push_back({callbackPtr, transType});
}
}
if (conn)
{
makeTrans(conn,
std::function<void(const std::shared_ptr<Transaction> &)>(
callback));
callback),
transType);
}
}
void DbClientImpl::makeTrans(
const DbConnectionPtr &conn,
std::function<void(const std::shared_ptr<Transaction> &)> &&callback)
std::function<void(const std::shared_ptr<Transaction> &)> &&callback,
TransactionType transType)
{
std::weak_ptr<DbClientImpl> weakThis = shared_from_this();
auto trans = std::make_shared<TransactionImpl>(
type_, conn, std::function<void(bool)>(), [weakThis, conn]() {
type_,
conn,
std::function<void(bool)>(),
[weakThis, conn]() {
auto thisPtr = weakThis.lock();
if (!thisPtr)
return;
@@ -306,7 +312,8 @@ void DbClientImpl::makeTrans(
});
thisPtr->handleNewTask(conn);
});
});
},
transType);
trans->doBegin();
if (timeout_ > 0.0)
{
@@ -317,13 +324,16 @@ void DbClientImpl::makeTrans(
}
std::shared_ptr<Transaction> DbClientImpl::newTransaction(
const std::function<void(bool)> &commitCallback) noexcept(false)
const std::function<void(bool)> &commitCallback,
TransactionType transType) noexcept(false)
{
std::promise<std::shared_ptr<Transaction>> pro;
auto f = pro.get_future();
newTransactionAsync([&pro](const std::shared_ptr<Transaction> &trans) {
pro.set_value(trans);
});
newTransactionAsync(
[&pro](const std::shared_ptr<Transaction> &trans) {
pro.set_value(trans);
},
transType);
auto trans = f.get();
if (!trans)
{
@@ -336,12 +346,15 @@ std::shared_ptr<Transaction> DbClientImpl::newTransaction(
void DbClientImpl::handleNewTask(const DbConnectionPtr &connPtr)
{
std::function<void(const std::shared_ptr<Transaction> &)> transCallback;
TransactionType transType{TransactionType::Deferred};
std::shared_ptr<SqlCmd> cmd;
{
std::lock_guard<std::mutex> guard(connectionsMutex_);
if (!transCallbacks_.empty())
{
transCallback = std::move(*(transCallbacks_.front()));
auto &entry = transCallbacks_.front();
transCallback = std::move(*entry.first);
transType = entry.second;
transCallbacks_.pop_front();
}
else if (!sqlCmdBuffer_.empty())
@@ -358,7 +371,7 @@ void DbClientImpl::handleNewTask(const DbConnectionPtr &connPtr)
}
if (transCallback)
{
makeTrans(connPtr, std::move(transCallback));
makeTrans(connPtr, std::move(transCallback), transType);
return;
}
if (cmd)
+12 -6
View File
@@ -52,10 +52,13 @@ class DbClientImpl : public DbClient,
&&exceptCallback) override;
std::shared_ptr<Transaction> newTransaction(
const std::function<void(bool)> &commitCallback =
std::function<void(bool)>()) noexcept(false) override;
std::function<void(bool)>(),
TransactionType transType =
TransactionType::Deferred) noexcept(false) override;
void newTransactionAsync(
const std::function<void(const std::shared_ptr<Transaction> &)>
&callback) override;
&callback,
TransactionType transType = TransactionType::Deferred) override;
bool hasAvailableConnections() const noexcept override;
void setTimeout(double timeout) override
@@ -78,16 +81,19 @@ class DbClientImpl : public DbClient,
void makeTrans(
const DbConnectionPtr &conn,
std::function<void(const std::shared_ptr<Transaction> &)> &&callback);
std::function<void(const std::shared_ptr<Transaction> &)> &&callback,
TransactionType transType = TransactionType::Deferred);
mutable std::mutex connectionsMutex_;
std::unordered_set<DbConnectionPtr> connections_;
std::unordered_set<DbConnectionPtr> readyConnections_;
std::unordered_set<DbConnectionPtr> busyConnections_;
std::list<std::shared_ptr<
std::function<void(const std::shared_ptr<Transaction> &)>>>
transCallbacks_;
using TransCallbackEntry =
std::pair<std::shared_ptr<std::function<void(
const std::shared_ptr<Transaction> &)>>,
TransactionType>;
std::list<TransCallbackEntry> transCallbacks_;
std::deque<std::shared_ptr<SqlCmd>> sqlCmdBuffer_;
+24 -12
View File
@@ -230,7 +230,8 @@ void DbClientLockFree::execSql(
}
std::shared_ptr<Transaction> DbClientLockFree::newTransaction(
const std::function<void(bool)> &) noexcept(false)
const std::function<void(bool)> &,
TransactionType) noexcept(false)
{
// Don't support transaction;
LOG_ERROR
@@ -241,7 +242,8 @@ std::shared_ptr<Transaction> DbClientLockFree::newTransaction(
}
void DbClientLockFree::newTransactionAsync(
const std::function<void(const std::shared_ptr<Transaction> &)> &callback)
const std::function<void(const std::shared_ptr<Transaction> &)> &callback,
TransactionType transType)
{
loop_->assertInLoopThread();
for (auto &conn : connections_)
@@ -250,7 +252,8 @@ void DbClientLockFree::newTransactionAsync(
{
makeTrans(conn,
std::function<void(const std::shared_ptr<Transaction> &)>(
callback));
callback),
transType);
return;
}
}
@@ -272,7 +275,7 @@ void DbClientLockFree::newTransactionAsync(
iter != transCallbacks_.end();
++iter)
{
if (cbPtr == *iter)
if (cbPtr == iter->first)
{
transCallbacks_.erase(iter);
break;
@@ -292,16 +295,20 @@ void DbClientLockFree::newTransactionAsync(
*newCallbackPtr = callbackPtr;
timeoutFlagPtr->runTimer();
}
transCallbacks_.push_back(callbackPtr);
transCallbacks_.push_back({callbackPtr, transType});
}
void DbClientLockFree::makeTrans(
const DbConnectionPtr &conn,
std::function<void(const std::shared_ptr<Transaction> &)> &&callback)
std::function<void(const std::shared_ptr<Transaction> &)> &&callback,
TransactionType transType)
{
std::weak_ptr<DbClientLockFree> weakThis = shared_from_this();
auto trans = std::make_shared<TransactionImpl>(
type_, conn, std::function<void(bool)>(), [weakThis, conn]() {
type_,
conn,
std::function<void(bool)>(),
[weakThis, conn]() {
auto thisPtr = weakThis.lock();
if (!thisPtr)
return;
@@ -312,9 +319,11 @@ void DbClientLockFree::makeTrans(
}
if (!thisPtr->transCallbacks_.empty())
{
auto callback = std::move(thisPtr->transCallbacks_.front());
auto &entry = thisPtr->transCallbacks_.front();
auto nextCallback = std::move(*entry.first);
auto nextType = entry.second;
thisPtr->transCallbacks_.pop_front();
thisPtr->makeTrans(conn, std::move(*callback));
thisPtr->makeTrans(conn, std::move(nextCallback), nextType);
return;
}
@@ -342,7 +351,8 @@ void DbClientLockFree::makeTrans(
break;
}
}
});
},
transType);
transSet_.insert(conn);
trans->doBegin();
if (timeout_ > 0.0)
@@ -360,9 +370,11 @@ void DbClientLockFree::handleNewTask(const DbConnectionPtr &conn)
if (!transCallbacks_.empty())
{
auto callback = std::move(transCallbacks_.front());
auto &entry = transCallbacks_.front();
auto callback = std::move(*entry.first);
auto transType = entry.second;
transCallbacks_.pop_front();
makeTrans(conn, std::move(*callback));
makeTrans(conn, std::move(callback), transType);
return;
}
+12 -6
View File
@@ -55,10 +55,13 @@ class DbClientLockFree : public DbClient,
&&exceptCallback) override;
std::shared_ptr<Transaction> newTransaction(
const std::function<void(bool)> &commitCallback =
std::function<void(bool)>()) noexcept(false) override;
std::function<void(bool)>(),
TransactionType transType =
TransactionType::Deferred) noexcept(false) override;
void newTransactionAsync(
const std::function<void(const std::shared_ptr<Transaction> &)>
&callback) override;
&callback,
TransactionType transType = TransactionType::Deferred) override;
bool hasAvailableConnections() const noexcept override;
void setTimeout(double timeout) override
@@ -78,15 +81,18 @@ class DbClientLockFree : public DbClient,
std::unordered_set<DbConnectionPtr> transSet_;
std::deque<std::shared_ptr<SqlCmd>> sqlCmdBuffer_;
std::list<std::shared_ptr<
std::function<void(const std::shared_ptr<Transaction> &)>>>
transCallbacks_;
using TransCallbackEntry =
std::pair<std::shared_ptr<std::function<void(
const std::shared_ptr<Transaction> &)>>,
TransactionType>;
std::list<TransCallbackEntry> transCallbacks_;
double timeout_{-1.0};
void makeTrans(
const DbConnectionPtr &conn,
std::function<void(const std::shared_ptr<Transaction> &)> &&callback);
std::function<void(const std::shared_ptr<Transaction> &)> &&callback,
TransactionType transType = TransactionType::Deferred);
void execSqlWithTimeout(
const char *sql,
size_t sqlLength,
+53 -19
View File
@@ -23,11 +23,13 @@ using namespace drogon;
TransactionImpl::TransactionImpl(ClientType type,
const DbConnectionPtr &connPtr,
std::function<void(bool)> commitCallback,
std::function<void()> usedUpCallback)
std::function<void()> usedUpCallback,
TransactionType transType)
: connectionPtr_(connPtr),
usedUpCallback_(std::move(usedUpCallback)),
loop_(connPtr->loop()),
commitCallback_(std::move(commitCallback))
commitCallback_(std::move(commitCallback)),
transactionType_(transType)
{
type_ = type;
}
@@ -203,6 +205,8 @@ void TransactionImpl::execNewTask()
{
loop_->assertInLoopThread();
thisPtr_.reset();
if (!isWorking_)
return;
assert(isWorking_);
if (!isCommitedOrRolledback_)
{
@@ -244,27 +248,51 @@ void TransactionImpl::execNewTask()
else
{
isWorking_ = false;
if (!sqlCmdBuffer_.empty())
failBufferedCommands(std::make_exception_ptr(
TransactionRollback("The transaction has been rolled back")));
releaseConnection();
}
}
void TransactionImpl::releaseConnection()
{
if (usedUpCallback_)
{
usedUpCallback_();
usedUpCallback_ = std::function<void()>();
}
}
void TransactionImpl::failBufferedCommands(const std::exception_ptr &ePtr)
{
std::list<SqlCmdPtr> pendingCmds;
pendingCmds.swap(sqlCmdBuffer_);
for (auto &cmd : pendingCmds)
{
cmd->thisPtr_.reset();
if (cmd->exceptionCallback_)
{
auto exceptPtr = std::make_exception_ptr(
TransactionRollback("The transaction has been rolled back"));
for (auto const &cmd : sqlCmdBuffer_)
{
if (cmd->exceptionCallback_)
{
cmd->exceptionCallback_(exceptPtr);
}
}
sqlCmdBuffer_.clear();
}
if (usedUpCallback_)
{
usedUpCallback_();
usedUpCallback_ = std::function<void()>();
cmd->exceptionCallback_(ePtr);
}
}
}
const char *TransactionImpl::beginSql() const noexcept
{
if (type_ != ClientType::Sqlite3)
return "begin";
switch (transactionType_)
{
case TransactionType::Immediate:
return "begin immediate";
case TransactionType::Exclusive:
return "begin exclusive";
default:
return "begin";
}
}
void TransactionImpl::doBegin()
{
loop_->queueInLoop([thisPtr = shared_from_this()]() {
@@ -280,7 +308,7 @@ void TransactionImpl::doBegin()
thisPtr->isWorking_ = true;
thisPtr->thisPtr_ = thisPtr;
thisPtr->connectionPtr_->execSql(
"begin",
thisPtr->beginSql(),
0,
{},
{},
@@ -289,6 +317,12 @@ void TransactionImpl::doBegin()
[thisPtr](const std::exception_ptr &) {
LOG_ERROR << "Error occurred in transaction begin";
thisPtr->isCommitedOrRolledback_ = true;
thisPtr->isWorking_ = false;
thisPtr->thisPtr_.reset();
thisPtr->failBufferedCommands(std::make_exception_ptr(
TransactionRollback("Transaction begin failed, cannot "
"execute queued SQL")));
thisPtr->releaseConnection();
});
});
}
+10 -3
View File
@@ -30,7 +30,8 @@ class TransactionImpl : public Transaction,
TransactionImpl(ClientType type,
const DbConnectionPtr &connPtr,
std::function<void(bool)> commitCallback,
std::function<void()> usedUpCallback);
std::function<void()> usedUpCallback,
TransactionType transType = TransactionType::Deferred);
~TransactionImpl() override;
void rollback() override;
@@ -113,14 +114,16 @@ class TransactionImpl : public Transaction,
std::function<void(const std::exception_ptr &)> &&exceptCallback);
std::shared_ptr<Transaction> newTransaction(
const std::function<void(bool)> &) noexcept(false) override
const std::function<void(bool)> &,
TransactionType) noexcept(false) override
{
return shared_from_this();
}
void newTransactionAsync(
const std::function<void(const std::shared_ptr<Transaction> &)>
&callback) override
&callback,
TransactionType) override
{
callback(shared_from_this());
}
@@ -129,6 +132,8 @@ class TransactionImpl : public Transaction,
bool isCommitedOrRolledback_{false};
bool isWorking_{false};
void execNewTask();
void releaseConnection();
void failBufferedCommands(const std::exception_ptr &ePtr);
struct SqlCmd
{
@@ -149,10 +154,12 @@ class TransactionImpl : public Transaction,
friend class DbClientImpl;
friend class DbClientLockFree;
void doBegin();
const char *beginSql() const noexcept;
trantor::EventLoop *loop_;
std::function<void(bool)> commitCallback_;
std::shared_ptr<TransactionImpl> thisPtr_;
double timeout_{-1.0};
TransactionType transactionType_{TransactionType::Deferred};
};
} // namespace orm
} // namespace drogon
+185 -6
View File
@@ -2741,11 +2741,9 @@ DROGON_TEST(MySQLTest)
#endif
#if USE_SQLITE3
DbClientPtr sqlite3Client;
DROGON_TEST(SQLite3Test)
{
auto &clientPtr = sqlite3Client;
auto clientPtr = DbClient::newSqlite3Client("filename=:memory:", 1);
REQUIRE(clientPtr != nullptr);
// Prepare the test environment
@@ -4063,6 +4061,190 @@ DROGON_TEST(SQLite3Test)
}
#endif
#if USE_SQLITE3
DROGON_TEST(SQLite3TransactionTypeTest)
{
auto clientPtr = DbClient::newSqlite3Client("filename=:memory:", 1);
REQUIRE(clientPtr != nullptr);
// Ensure the test table exists
try
{
clientPtr->execSqlSync(
"CREATE TABLE IF NOT EXISTS trans_type_test "
"(id INTEGER PRIMARY KEY, val INTEGER NOT NULL)");
clientPtr->execSqlSync("DELETE FROM trans_type_test");
}
catch (const DrogonDbException &e)
{
FAULT("sqlite3 - TransactionType setup what():", e.base().what());
return;
}
// --- Deferred (default) ---
{
try
{
auto trans = clientPtr->newTransaction(TransactionType::Deferred);
trans->execSqlSync(
"INSERT INTO trans_type_test(id, val) VALUES(1, 10)");
// trans commits on destruction
}
catch (const DrogonDbException &e)
{
FAULT("sqlite3 - TransactionType::Deferred what():",
e.base().what());
return;
}
auto r = clientPtr->execSqlSync(
"SELECT val FROM trans_type_test WHERE id=1");
MANDATE(r.size() == 1);
MANDATE(r[0][0].as<int>() == 10);
SUCCESS();
}
// --- Immediate ---
{
try
{
auto trans = clientPtr->newTransaction(TransactionType::Immediate);
trans->execSqlSync(
"INSERT INTO trans_type_test(id, val) VALUES(2, 20)");
}
catch (const DrogonDbException &e)
{
FAULT("sqlite3 - TransactionType::Immediate what():",
e.base().what());
return;
}
auto r = clientPtr->execSqlSync(
"SELECT val FROM trans_type_test WHERE id=2");
MANDATE(r.size() == 1);
MANDATE(r[0][0].as<int>() == 20);
SUCCESS();
}
// --- Exclusive ---
{
try
{
auto trans = clientPtr->newTransaction(TransactionType::Exclusive);
trans->execSqlSync(
"INSERT INTO trans_type_test(id, val) VALUES(3, 30)");
}
catch (const DrogonDbException &e)
{
FAULT("sqlite3 - TransactionType::Exclusive what():",
e.base().what());
return;
}
auto r = clientPtr->execSqlSync(
"SELECT val FROM trans_type_test WHERE id=3");
MANDATE(r.size() == 1);
MANDATE(r[0][0].as<int>() == 30);
SUCCESS();
}
// --- Rollback works correctly with Immediate ---
{
try
{
auto trans = clientPtr->newTransaction(TransactionType::Immediate);
trans->execSqlSync(
"INSERT INTO trans_type_test(id, val) VALUES(99, 99)");
trans->rollback();
}
catch (const DrogonDbException &e)
{
FAULT("sqlite3 - TransactionType::Immediate rollback what():",
e.base().what());
return;
}
auto r = clientPtr->execSqlSync(
"SELECT val FROM trans_type_test WHERE id=99");
MANDATE(r.size() == 0);
SUCCESS();
}
}
// Verify the locking mode is actually used by testing observable SQLite
// locking behaviour. BEGIN IMMEDIATE acquires a RESERVED lock upfront, so a
// second concurrent BEGIN IMMEDIATE on another connection to the same
// database must fail with SQLITE_BUSY. If plain BEGIN were used instead, the
// second connection would succeed (only a SHARED lock is held until the first
// write).
DROGON_TEST(SQLite3TransactionTypeLockingTest)
{
// A pool of 2 connections to a shared file-based database gives us two
// independent SQLite connections that observe each other's locks.
const auto nonce =
std::chrono::steady_clock::now().time_since_epoch().count();
const auto dbPath =
"drogon_trans_type_lock_test_" + std::to_string(nonce) + ".db";
std::remove(dbPath.c_str());
auto pool = DbClient::newSqlite3Client("filename=" + dbPath, 2);
// WAL mode is required: it changes BEGIN IMMEDIATE from acquiring a
// RESERVED lock to acquiring the WAL write lock. This matches production
// usage and makes the busy semantics more predictable — only one writer
// is ever permitted and SQLITE_BUSY is returned immediately (no timeout
// retry) when a second BEGIN IMMEDIATE is attempted.
pool->execSqlSync("PRAGMA journal_mode=WAL");
// No retry delay: SQLITE_BUSY must surface as an exception immediately.
pool->execSqlSync("PRAGMA busy_timeout=0");
pool->execSqlSync(
"CREATE TABLE IF NOT EXISTS lock_test (id INTEGER PRIMARY KEY)");
std::shared_ptr<Transaction> transA;
// Hold an IMMEDIATE transaction on connection A.
try
{
transA = pool->newTransaction(TransactionType::Immediate);
// doBegin() is asynchronous — the BEGIN IMMEDIATE is queued to the
// connection's event loop. Run a synchronous query through the
// transaction to flush the queue; once execSqlSync returns, the
// RESERVED lock is definitely held.
transA->execSqlSync("SELECT 1");
}
catch (const DrogonDbException &e)
{
std::remove(dbPath.c_str());
FAULT("sqlite3 - TransactionType::Immediate locking setup what():",
e.base().what());
return;
}
// Connection B attempting BEGIN IMMEDIATE must fail because A already
// holds the RESERVED lock. SQLite's default busy_timeout is 0.
bool gotBusy = false;
try
{
auto transB = pool->newTransaction(TransactionType::Immediate);
transB->execSqlSync("SELECT 1");
transB->rollback();
}
catch (const DrogonDbException &)
{
gotBusy = true;
}
transA->rollback();
std::remove(dbPath.c_str());
if (gotBusy)
{
SUCCESS();
}
else
{
FAULT(
"sqlite3 - TransactionType::Immediate locking: second BEGIN "
"IMMEDIATE should have failed while the first was held, but it "
"succeeded. This means BEGIN IMMEDIATE is not being sent.");
}
}
#endif
using namespace drogon;
int main(int argc, char **argv)
@@ -4079,9 +4261,6 @@ int main(int argc, char **argv)
"client_encoding=utf8",
1,
true);
#endif
#if USE_SQLITE3
sqlite3Client = DbClient::newSqlite3Client("filename=:memory:", 1);
#endif
const int testStatus = test::run(argc, argv);
return testStatus;
+134 -32
View File
@@ -23,46 +23,147 @@ else
fi
echo "drogon_ctl_exec: " ${drogon_ctl_exec}
#Make integration_test_server run as a daemon
if [ "X$os" = "Xwindows" ]; then
integration_test_client_exec=./integration_test_client.exe
integration_test_server_exec=./integration_test_server.exe
else
integration_test_client_exec=./integration_test_client
integration_test_server_exec=./integration_test_server
fi
function update_config_line()
{
local key="$1"
local value="$2"
local file="$3"
sed -i.bak -e "s/\"${key}\".*$/\"${key}\": ${value},/" "$file"
rm -f "$file.bak"
}
function cleanup_integration_test_server()
{
if [ "X$os" = "Xwindows" ]; then
taskkill //F //IM integration_test_server.exe > /dev/null 2>&1 || true
else
killall integration_test_server > /dev/null 2>&1 || true
pkill -f '/integration_test_server$' > /dev/null 2>&1 || true
fi
}
function wait_for_url()
{
local url="$1"
local timeout_seconds="$2"
local curl_args=(--silent --show-error --output /dev/null --max-time 2)
if [ "$url" != "${url#https://}" ]; then
curl_args+=(--insecure)
fi
local attempt=0
while [ $attempt -lt $timeout_seconds ]; do
if curl "${curl_args[@]}" "$url"; then
return 0
fi
attempt=$((attempt + 1))
sleep 1
done
return 1
}
function wait_for_integration_test_server()
{
local server_pid="$1"
local server_log="$2"
if ! wait_for_url "http://127.0.0.1:8848/" 30; then
echo "Timed out waiting for integration_test_server to accept HTTP requests"
if kill -0 "$server_pid" > /dev/null 2>&1; then
echo "integration_test_server is still running, recent log output:"
else
echo "integration_test_server exited before becoming ready, recent log output:"
fi
if [ -f "$server_log" ]; then
tail -n 50 "$server_log"
fi
return 1
fi
wait_for_url "https://127.0.0.1:8849/" 5 > /dev/null 2>&1 || true
return 0
}
function get_cpu_count()
{
if command -v nproc > /dev/null 2>&1; then
nproc
return
fi
if command -v getconf > /dev/null 2>&1; then
getconf _NPROCESSORS_ONLN
return
fi
if command -v sysctl > /dev/null 2>&1; then
sysctl -n hw.logicalcpu
return
fi
echo 1
}
trap cleanup_integration_test_server EXIT
# Run the integration test server in the background and wait until it is ready.
function do_integration_test()
{
pushd $test_root
if [ "X$os" = "Xlinux" ]; then
sed -i -e "s/\"run_as_daemon.*$/\"run_as_daemon\": true\,/" config.example.json
fi
sed -i -e "s/\"relaunch_on_error.*$/\"relaunch_on_error\": true\,/" config.example.json
sed -i -e "s/\"threads_num.*$/\"threads_num\": 0\,/" config.example.json
sed -i -e "s/\"use_brotli.*$/\"use_brotli\": true\,/" config.example.json
pushd "$test_root"
update_config_line "run_as_daemon" "false" config.example.json
update_config_line "relaunch_on_error" "false" config.example.json
update_config_line "number_of_threads" "1" config.example.json
update_config_line "use_brotli" "true" config.example.json
if [ "$1" = "stream_mode" ]; then
sed -i -e "s/\"enable_request_stream.*$/\"enable_request_stream\": true\,/" config.example.json
update_config_line "enable_request_stream" "true" config.example.json
else
sed -i -e "s/\"enable_request_stream.*$/\"enable_request_stream\": false\,/" config.example.json
update_config_line "enable_request_stream" "false" config.example.json
fi
if [ ! -f "integration_test_client" ]; then
if [ ! -f "$integration_test_client_exec" ]; then
echo "Build failed"
exit -1
fi
if [ ! -f "integration_test_server" ]; then
if [ ! -f "$integration_test_server_exec" ]; then
echo "Build failed"
exit -1
fi
killall -9 integration_test_server
./integration_test_server &
cleanup_integration_test_server
sleep 4
local server_log=integration_test_server.log
rm -f "$server_log"
"$integration_test_server_exec" > "$server_log" 2>&1 &
local server_pid=$!
if ! wait_for_integration_test_server "$server_pid" "$server_log"; then
exit -1
fi
echo "Running the integration test $1"
./integration_test_client -s
"$integration_test_client_exec" -s
if [ $? -ne 0 ]; then
echo "Integration test failed $1"
if [ -f "$server_log" ]; then
tail -n 50 "$server_log"
fi
exit -1
fi
killall -9 integration_test_server
cleanup_integration_test_server
popd
}
@@ -70,7 +171,7 @@ function do_integration_test()
function do_drogon_ctl_test()
{
echo "Testing drogon_ctl"
pushd $test_root
pushd "$test_root"
rm -rf drogon_test
${drogon_ctl_exec} create project drogon_test
@@ -122,22 +223,23 @@ function do_drogon_ctl_test()
make_flags=''
cmake_gen=''
parallel=1
cpu_count=$(get_cpu_count)
# simulate ninja's parallelism
case $(nproc) in
case $cpu_count in
1)
parallel=$(($(nproc) + 1))
parallel=$((cpu_count + 1))
;;
2)
parallel=$(($(nproc) + 1))
parallel=$((cpu_count + 1))
;;
*)
parallel=$(($(nproc) + 2))
parallel=$((cpu_count + 2))
;;
esac
if [ "X$os" = "Xlinux" ]; then
if [ -f /bin/ninja ]; then
if command -v ninja > /dev/null 2>&1; then
cmake_gen='-G Ninja'
else
make_flags="$make_flags -j$parallel"
@@ -162,11 +264,11 @@ function do_drogon_ctl_test()
exit -1
fi
if [ "X$os" = "Xlinux" ]; then
if [ ! -f "drogon_test" ]; then
echo "Failed to build drogon_test"
exit -1
fi
if [ "X$os" = "Xlinux" ]; then
if [ ! -f "drogon_test" ]; then
echo "Failed to build drogon_test"
exit -1
fi
else
if [ ! -f "Debug\drogon_test.exe" ]; then
echo "Failed to build drogon_test"
@@ -183,7 +285,7 @@ function do_drogon_ctl_test()
function do_unittest()
{
echo "Unit testing"
pushd $src_dir/build
pushd "$src_dir/build"
ctest . --output-on-failure
if [ $? -ne 0 ]; then
@@ -195,7 +297,7 @@ function do_unittest()
function do_db_test()
{
pushd $src_dir/build
pushd "$src_dir/build"
if [ -f "./orm_lib/tests/db_test" ]; then
echo "Test database"
./orm_lib/tests/db_test -s
@@ -246,9 +348,9 @@ function do_db_test()
fi
}
if ! drogon_ctl -v > /dev/null 2>&1
if [ ! -f "$drogon_ctl_exec" ]
then
echo "Warning: No drogon_ctl, skip integration test and drogon_ctl test"
echo "Warning: No built drogon_ctl, skip integration test and drogon_ctl test"
else
do_integration_test
do_integration_test stream_mode
Vendored Executable → Regular
+12 -7
View File
@@ -5,8 +5,17 @@ option(BUILD_DOC "Build Doxygen documentation" OFF)
option(BUILD_C-ARES "Build C-ARES" ON)
option(BUILD_TESTING "Build tests" OFF)
option(BUILD_SHARED_LIBS "Build trantor as a shared lib" OFF)
option(TRANTOR_USE_TLS
"TLS provider for trantor. Valid options are 'openssl', 'botan' or '' (let the build scripr decide)" ""
set(TRANTOR_USE_TLS
""
CACHE STRING "TLS provider for trantor. Valid options are 'openssl', 'botan', 'none' or '' (auto-detect)"
)
set_property(
CACHE TRANTOR_USE_TLS
PROPERTY STRINGS
""
openssl
botan
none
)
option(USE_SPDLOG "Allow using the spdlog logging library" OFF)
@@ -14,7 +23,7 @@ list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake_modules/)
set(TRANTOR_MAJOR_VERSION 1)
set(TRANTOR_MINOR_VERSION 5)
set(TRANTOR_PATCH_VERSION 26)
set(TRANTOR_PATCH_VERSION 28)
set(TRANTOR_VERSION ${TRANTOR_MAJOR_VERSION}.${TRANTOR_MINOR_VERSION}.${TRANTOR_PATCH_VERSION})
include(GNUInstallDirs)
@@ -158,10 +167,6 @@ else(WIN32)
set(TRANTOR_SOURCES ${TRANTOR_SOURCES} trantor/net/inner/FileBufferNodeUnix.cc)
endif(WIN32)
# Somehow the default value of TRANTOR_USE_TLS is OFF
if(TRANTOR_USE_TLS STREQUAL OFF)
set(TRANTOR_USE_TLS "")
endif()
set(VALID_TLS_PROVIDERS "openssl" "botan" "none")
list(
FIND
+29 -1
View File
@@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
## [1.5.28] - 2026-05-06
### Fixed
- Avoid abort on closeWrite shutdown failure.
## [1.5.27] - 2026-05-06
### Changed
- Add automatic SSL.
- Add getter for `TcpConnection::closeCallback_`.
- Remove spurious executable permissions from non-script sources.
### Fixed
- Fix `TRANTOR_USE_TLS` cache setting in CMake.
- Fix TLS implementation quirks.
- Fix server-side mTLS client certificate hostname validation.
## [1.5.26] - 2026-01-26
### Changed
@@ -742,7 +766,11 @@ All notable changes to this project will be documented in this file.
## [1.0.0-rc1] - 2019-06-11
[Unreleased]: https://github.com/an-tao/trantor/compare/v1.5.26...HEAD
[Unreleased]: https://github.com/an-tao/trantor/compare/v1.5.28...HEAD
[1.5.28]: https://github.com/an-tao/trantor/compare/v1.5.27...v1.5.28
[1.5.27]: https://github.com/an-tao/trantor/compare/v1.5.26...v1.5.27
[1.5.26]: https://github.com/an-tao/trantor/compare/v1.5.25...v1.5.26
Vendored Executable → Regular
View File
@@ -351,6 +351,10 @@ class TRANTOR_EXPORT TcpConnection
{
closeCallback_ = std::move(cb);
}
CloseCallback getCloseCallback() const
{
return closeCallback_;
}
void setSSLErrorCallback(const SSLErrorCallback &cb)
{
sslErrorCallback_ = cb;
@@ -367,6 +371,8 @@ class TRANTOR_EXPORT TcpConnection
size_t timeout,
const std::shared_ptr<TimingWheel> &timingWheel) = 0;
virtual void forwardToTLSBuffer(MsgBuffer *buffer) = 0;
protected:
// callbacks
RecvMessageCallback recvMsgCallback_;
View File
@@ -203,6 +203,12 @@ class TcpConnectionImpl : public TcpConnection,
timingWheel->insertEntry(timeout, entry);
}
void forwardToTLSBuffer(MsgBuffer *buffer) override
{
if (tlsProviderPtr_)
tlsProviderPtr_->recvData(buffer);
}
private:
/// Internal use only.
@@ -474,8 +474,6 @@ SSLContextPtr trantor::newSSLContext(const TLSPolicy &policy, bool server)
ctx->certStore =
std::make_shared<Botan::Flatfile_Certificate_Store>(
policy.getCaPath());
if (server)
ctx->requireClientCert = true;
}
else if (policy.getUseSystemCertStore())
{
@@ -484,6 +482,8 @@ SSLContextPtr trantor::newSSLContext(const TLSPolicy &policy, bool server)
ctx->certStore = systemCertStore;
}
}
if (server && policy.getValidate() && !policy.getCaPath().empty())
ctx->requireClientCert = true;
if (policy.getUseOldTLS())
LOG_WARN << "SSLPloicy have set useOldTLS to true. BUt Botan does not "
@@ -8,12 +8,10 @@
#include <openssl/bio.h>
#include <openssl/x509v3.h>
#include <fstream>
#include <memory>
#include <mutex>
#include <list>
#include <unordered_map>
#include <array>
#include <limits>
#include "callbacks.h"
@@ -70,62 +68,6 @@ inline bool loadWindowsSystemCert(X509_STORE *store)
}
#endif
inline bool verifyCommonName(X509 *cert, const std::string &hostname)
{
X509_NAME *subjectName = X509_get_subject_name(cert);
if (subjectName != nullptr)
{
std::array<char, BUFSIZ> name;
auto length = X509_NAME_get_text_by_NID(subjectName,
NID_commonName,
name.data(),
(int)name.size());
if (length == -1)
return false;
return utils::verifySslName(std::string(name.begin(),
name.begin() + length),
hostname);
}
return false;
}
inline bool verifyAltName(X509 *cert, const std::string &hostname)
{
bool good = false;
auto altNames = static_cast<const struct stack_st_GENERAL_NAME *>(
X509_get_ext_d2i(cert, NID_subject_alt_name, nullptr, nullptr));
if (altNames)
{
int numNames = sk_GENERAL_NAME_num(altNames);
for (int i = 0; i < numNames && !good; i++)
{
auto val = sk_GENERAL_NAME_value(altNames, i);
if (val->type != GEN_DNS)
{
LOG_WARN << "Name using IP addresses are not supported. Open "
"an issue if you need that feature";
continue;
}
#if (OPENSSL_VERSION_NUMBER >= 0x10100000L)
auto name = (const char *)ASN1_STRING_get0_data(val->d.ia5);
#else
auto name = (const char *)ASN1_STRING_data(val->d.ia5);
#endif
auto name_len = (size_t)ASN1_STRING_length(val->d.ia5);
good = utils::verifySslName(std::string(name, name + name_len),
hostname);
}
}
GENERAL_NAMES_free((STACK_OF(GENERAL_NAME) *)altNames);
return good;
}
static bool validatePeerCertificate(SSL *ssl,
X509 *cert,
const std::string &hostname,
@@ -136,12 +78,16 @@ static bool validatePeerCertificate(SSL *ssl,
assert(cert != nullptr);
LOG_TRACE << "Validating peer certificate";
if (isServer)
if (!isServer)
{
bool domainIsValid =
verifyCommonName(cert, hostname) || verifyAltName(cert, hostname);
if (!domainIsValid)
const int rc =
X509_check_host(cert, hostname.data(), hostname.size(), 0, nullptr);
if (rc != 1)
{
LOG_TRACE << "Peer certificate does not match hostname: "
<< hostname;
return false;
}
}
auto result = SSL_get_verify_result(ssl);
@@ -423,16 +369,20 @@ class SessionManager
#endif
}
// Returns a session with an additional reference held by the caller.
// Caller must SSL_SESSION_free() when done. Required because the entry
// in sessionMap_ may be evicted/replaced/expired by another thread the
// moment we release the mutex, so the SessionManager's reference is not
// a stable ownership root for the returned pointer.
SSL_SESSION *get(const std::string &hostname, InetAddress peerAddr)
{
std::lock_guard<std::mutex> lock(mutex_);
auto key = toKey(hostname, peerAddr);
auto it = sessionMap_.find(key);
if (it != sessionMap_.end())
{
return it->second->session;
}
return nullptr;
auto it = sessionMap_.find(toKey(hostname, peerAddr));
if (it == sessionMap_.end())
return nullptr;
SSL_SESSION *s = it->second->session;
SSL_SESSION_up_ref(s);
return s;
}
void removeExcessSession()
@@ -529,7 +479,9 @@ struct OpenSSLProvider : public TLSProvider, public NonCopyable
conn_->peerAddr());
if (cachedSession)
{
// SSL_set_session takes its own reference; release ours.
SSL_set_session(ssl_, cachedSession);
SSL_SESSION_free(cachedSession);
}
SSL_set_connect_state(ssl_);
}
@@ -671,7 +623,10 @@ struct OpenSSLProvider : public TLSProvider, public NonCopyable
cert,
policyPtr_->getHostname(),
policyPtr_->getAllowBrokenChain(),
contextPtr_->isServer);
!contextPtr_
->isServer); // From the server's point of view,
// the client certificate is verified
// and vice versa
if (!valid)
{
LOG_TRACE
@@ -0,0 +1,54 @@
#include <trantor/net/TcpClient.h>
#include <trantor/utils/Logger.h>
#include <trantor/net/EventLoopThread.h>
#include <string>
#include <iostream>
#include <atomic>
using namespace trantor;
#define USE_IPV6 0
int main()
{
trantor::Logger::setLogLevel(trantor::Logger::kDebug);
LOG_DEBUG << "TcpClient class test!";
EventLoop loop;
#if USE_IPV6
InetAddress serverAddr("::1", 8888, true);
#else
InetAddress serverAddr("127.0.0.1", 8888);
#endif
std::shared_ptr<trantor::TcpClient> client[10];
std::atomic_int connCount;
connCount = 1;
for (int i = 0; i < 1; ++i)
{
client[i] = std::make_shared<trantor::TcpClient>(&loop,
serverAddr,
"tcpclienttest");
auto policy = TLSPolicy::defaultClientPolicy();
policy->setValidate(false);
client[i]->enableSSL(std::move(policy));
client[i]->setConnectionCallback(
[i, &loop, &connCount](const TcpConnectionPtr &conn) {
if (conn->connected())
{
LOG_DEBUG << i << " connected";
conn->send("Hello");
}
else
{
LOG_DEBUG << i << " disconnected";
--connCount;
if (connCount == 0)
loop.quit();
}
});
client[i]->setMessageCallback(
[](const TcpConnectionPtr &conn, MsgBuffer *buf) {
auto msg = std::string(buf->peek(), buf->readableBytes());
LOG_INFO << msg;
buf->retrieveAll();
});
client[i]->connect();
}
loop.loop();
}
@@ -0,0 +1,63 @@
#include <trantor/net/TcpServer.h>
#include <trantor/utils/Logger.h>
#include <trantor/net/EventLoopThread.h>
#include <string>
#include <iostream>
using namespace trantor;
#define USE_IPV6 0
bool has_ssl(MsgBuffer *buffer)
{
if (buffer->readableBytes() < 3)
return false;
const char *data = buffer->peek();
unsigned char byte1 = static_cast<unsigned char>(data[0]);
unsigned char byte2 = static_cast<unsigned char>(data[1]);
unsigned char byte3 = static_cast<unsigned char>(data[2]);
return (byte1 == 0x16) && (byte2 == 0x03) && (byte3 == 0x01);
}
int main()
{
LOG_DEBUG << "test start";
Logger::setLogLevel(Logger::kDebug);
EventLoopThread loopThread;
loopThread.run();
#if USE_IPV6
InetAddress addr(8888, true, true);
#else
InetAddress addr(8888);
#endif
TcpServer server(loopThread.getLoop(), addr, "test");
// auto ctx = newSSLServerContext("server.pem", "server.pem", {});
LOG_INFO << "start";
server.setRecvMessageCallback(
[](const TcpConnectionPtr &connectionPtr, MsgBuffer *buffer) {
if (has_ssl(buffer))
{
LOG_DEBUG << "SSL data received";
auto policy =
TLSPolicy::defaultServerPolicy("server.crt", "server.key");
connectionPtr->startEncryption(policy, true);
connectionPtr->forwardToTLSBuffer(buffer);
return;
}
LOG_DEBUG << std::string{buffer->peek(), buffer->readableBytes()};
connectionPtr->send(*buffer);
buffer->retrieveAll();
connectionPtr->shutdown();
});
server.setConnectionCallback([](const TcpConnectionPtr &connPtr) {
if (connPtr->connected())
{
LOG_DEBUG << "New connection";
}
else if (connPtr->disconnected())
{
LOG_DEBUG << "connection disconnected";
}
});
server.setIoLoopNum(3);
server.start();
loopThread.wait();
}
@@ -23,6 +23,8 @@ add_executable(logger_macro_test LoggerMacroTest.cc)
add_executable(delayed_ssl_server_test DelayedSSLServerTest.cc)
add_executable(delayed_ssl_client_test DelayedSSLClientTest.cc)
add_executable(tcp_asyncstream_server_test TcpAsyncStreamServerTest.cc)
add_executable(automatic_ssl_server_test AutomaticSSLServerTest.cc)
add_executable(automatic_ssl_client_test AutomaticSSLClientTest.cc)
set(targets_list
ssl_server_test
ssl_client_test
@@ -49,6 +51,8 @@ set(targets_list
delayed_ssl_server_test
delayed_ssl_client_test
tcp_asyncstream_server_test
automatic_ssl_server_test
automatic_ssl_client_test
)
if(HAVE_SPDLOG)
View File
@@ -4,7 +4,6 @@ add_executable(inetaddress_unittest InetAddressUnittest.cc)
add_executable(date_unittest DateUnittest.cc)
add_executable(split_string_unittest splitStringUnittest.cc)
add_executable(string_encoding_unittest stringEncodingUnittest.cc)
add_executable(ssl_name_verify_unittest sslNameVerifyUnittest.cc)
add_executable(hash_unittest HashUnittest.cc)
set(UNITTEST_TARGETS
msgbuffer_unittest
@@ -12,7 +11,6 @@ set(UNITTEST_TARGETS
date_unittest
split_string_unittest
string_encoding_unittest
ssl_name_verify_unittest
hash_unittest
)
set_property(TARGET ${UNITTEST_TARGETS} PROPERTY CXX_STANDARD 14)
@@ -1,50 +0,0 @@
#include <trantor/utils/Utilities.h>
#include <gtest/gtest.h>
#include <iostream>
using namespace trantor;
using namespace trantor::utils;
TEST(sslNameCheck, baseCases)
{
EXPECT_EQ(verifySslName("example.com", "example.com"), true);
EXPECT_EQ(verifySslName("example.com", "example.org"), false);
EXPECT_EQ(verifySslName("example.com", "www.example.com"), false);
}
TEST(sslNameCheck, rfc6125Examples)
{
EXPECT_EQ(verifySslName("*.example.com", "foo.example.com"), true);
EXPECT_EQ(verifySslName("*.example.com", "foo.bar.example.com"), false);
EXPECT_EQ(verifySslName("*.example.com", "example.com"), false);
EXPECT_EQ(verifySslName("*bar.example.com", "foobar.example.com"), true);
EXPECT_EQ(verifySslName("baz*.example.com", "baz1.example.com"), true);
EXPECT_EQ(verifySslName("b*z.example.com", "buzz.example.com"), true);
}
TEST(sslNameCheck, rfcCounterExamples)
{
EXPECT_EQ(verifySslName("buz*.example.com", "buaz.example.com"), false);
EXPECT_EQ(verifySslName("*bar.example.com", "aaasdasbaz.example.com"),
false);
EXPECT_EQ(verifySslName("b*z.example.com", "baaaaaa.example.com"), false);
}
TEST(sslNameCheck, wildExamples)
{
EXPECT_EQ(verifySslName("datatracker.ietf.org", "datatracker.ietf.org"),
true);
EXPECT_EQ(verifySslName("*.nsysu.edu.tw", "nsysu.edu.tw"), false);
EXPECT_EQ(verifySslName("nsysu.edu.tw", "nsysu.edu.tw"), true);
}
TEST(sslNameCheck, edgeCase)
{
EXPECT_EQ(verifySslName(".example.com", "example.com"), false);
EXPECT_EQ(verifySslName("example.com.", "example.com."), true);
}
int main(int argc, char **argv)
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
View File
@@ -228,120 +228,6 @@ std::string fromWidePath(const std::wstring &wstrPath)
return toUtf8(srcPath);
}
bool verifySslName(const std::string &certName, const std::string &hostname)
{
if (certName.find('*') == std::string::npos)
{
return certName == hostname;
}
size_t firstDot = certName.find('.');
size_t hostFirstDot = hostname.find('.');
size_t pos, len, hostPos, hostLen;
if (firstDot != std::string::npos)
{
pos = firstDot + 1;
}
else
{
firstDot = pos = certName.size();
}
len = certName.size() - pos;
if (hostFirstDot != std::string::npos)
{
hostPos = hostFirstDot + 1;
}
else
{
hostFirstDot = hostPos = hostname.size();
}
hostLen = hostname.size() - hostPos;
// *. in the beginning of the cert name
if (certName.compare(0, firstDot, "*") == 0)
{
return certName.compare(pos, len, hostname, hostPos, hostLen) == 0;
}
// * in the left most. but other chars in the right
else if (certName[0] == '*')
{
// compare if `hostname` ends with `certName` but without the leftmost
// should be fine as domain names can't be that long
intmax_t hostnameIdx = hostname.size() - 1;
intmax_t certNameIdx = certName.size() - 1;
while (hostnameIdx >= 0 && certNameIdx != 0)
{
if (hostname[hostnameIdx] != certName[certNameIdx])
{
return false;
}
hostnameIdx--;
certNameIdx--;
}
if (certNameIdx != 0)
{
return false;
}
return true;
}
// * in the right of the first dot
else if (firstDot != 0 && certName[firstDot - 1] == '*')
{
if (certName.compare(pos, len, hostname, hostPos, hostLen) != 0)
{
return false;
}
for (size_t i = 0;
i < hostFirstDot && i < firstDot && certName[i] != '*';
i++)
{
if (hostname[i] != certName[i])
{
return false;
}
}
return true;
}
// else there's a * in the middle
else
{
if (certName.compare(pos, len, hostname, hostPos, hostLen) != 0)
{
return false;
}
for (size_t i = 0;
i < hostFirstDot && i < firstDot && certName[i] != '*';
i++)
{
if (hostname[i] != certName[i])
{
return false;
}
}
intmax_t hostnameIdx = hostFirstDot - 1;
intmax_t certNameIdx = firstDot - 1;
while (hostnameIdx >= 0 && certNameIdx >= 0 &&
certName[certNameIdx] != '*')
{
if (hostname[hostnameIdx] != certName[certNameIdx])
{
return false;
}
hostnameIdx--;
certNameIdx--;
}
return true;
}
assert(false && "This line should not be reached in verifySslName");
// should not reach
return certName == hostname;
}
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
@@ -171,15 +171,6 @@ inline std::string fromNativePath(const std::wstring &strPath)
return fromWidePath(strPath);
}
/**
* @brief Check if the name supplied by the SSL Cert matches a FQDN
* @param certName The name supplied by the SSL Cert
* @param hostName The FQDN to match
*
* @return true if matches. false otherwise
*/
bool verifySslName(const std::string &certName, const std::string &hostName);
/**
* @brief Returns the TLS backend used by trantor. Could be "None", "OpenSSL" or
* "Botan"