diff --git a/CMakeLists.txt b/CMakeLists.txt index f4b3b256..e4e4b08a 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,23 +55,23 @@ if (BUILD_DROGON_SHARED) VERSION ${DROGON_VERSION} SOVERSION ${DROGON_MAJOR_VERSION}) target_link_libraries(${PROJECT_NAME} PUBLIC Threads::Threads) - if(WIN32) - target_link_libraries(${PROJECT_NAME} PUBLIC Rpcrt4 ws2_32) - if(CMAKE_CXX_COMPILER_ID MATCHES MSVC) - # Ignore MSVC C4251 and C4275 warning of exporting std objects with no dll export - # We export class to facilitate maintenance, thus if you compile - # drogon on windows as a shared library, you will need to use - # exact same compiler for drogon and your app. - target_compile_options(${PROJECT_NAME} PUBLIC /wd4251 /wd4275) - endif() - endif() + if (WIN32) + target_link_libraries(${PROJECT_NAME} PUBLIC Rpcrt4 ws2_32) + if (CMAKE_CXX_COMPILER_ID MATCHES MSVC) + # Ignore MSVC C4251 and C4275 warning of exporting std objects with no dll export + # We export class to facilitate maintenance, thus if you compile + # drogon on windows as a shared library, you will need to use + # exact same compiler for drogon and your app. + target_compile_options(${PROJECT_NAME} PUBLIC /wd4251 /wd4275) + endif () + endif () else (BUILD_DROGON_SHARED) add_library(${PROJECT_NAME} STATIC) endif (BUILD_DROGON_SHARED) if (CMAKE_CXX_COMPILER_ID MATCHES GNU) target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Werror) -endif() +endif () include(GenerateExportHeader) generate_export_header(${PROJECT_NAME} EXPORT_FILE_NAME ${CMAKE_CURRENT_BINARY_DIR}/exports/drogon/exports.h) @@ -197,6 +197,7 @@ set(DROGON_SOURCES lib/src/NotFound.cc lib/src/PluginsManager.cc lib/src/SecureSSLRedirector.cc + lib/src/AccessLogger.cc lib/src/SessionManager.cc lib/src/StaticFileRouter.cc lib/src/TaskTimeoutFlag.cc @@ -281,10 +282,10 @@ endif (BUILD_REDIS) if (NOT Hiredis_FOUND) set(DROGON_SOURCES - ${DROGON_SOURCES} - lib/src/RedisClientSkipped.cc - lib/src/RedisResultSkipped.cc - lib/src/RedisClientManagerSkipped.cc) + ${DROGON_SOURCES} + lib/src/RedisClientSkipped.cc + lib/src/RedisResultSkipped.cc + lib/src/RedisClientManagerSkipped.cc) endif (NOT Hiredis_FOUND) if (BUILD_TESTING) @@ -334,24 +335,24 @@ if (COZ_PROFILING) endif (COZ_PROFILING) set(DROGON_SOURCES - ${DROGON_SOURCES} - orm_lib/src/ArrayParser.cc - orm_lib/src/Criteria.cc - orm_lib/src/DbClient.cc - orm_lib/src/DbClientImpl.cc - orm_lib/src/DbClientLockFree.cc - orm_lib/src/DbConnection.cc - orm_lib/src/Exception.cc - orm_lib/src/Field.cc - orm_lib/src/Result.cc - orm_lib/src/Row.cc - orm_lib/src/SqlBinder.cc - orm_lib/src/TransactionImpl.cc - orm_lib/src/RestfulController.cc) + ${DROGON_SOURCES} + orm_lib/src/ArrayParser.cc + orm_lib/src/Criteria.cc + orm_lib/src/DbClient.cc + orm_lib/src/DbClientImpl.cc + orm_lib/src/DbClientLockFree.cc + orm_lib/src/DbConnection.cc + orm_lib/src/Exception.cc + orm_lib/src/Field.cc + orm_lib/src/Result.cc + orm_lib/src/Row.cc + orm_lib/src/SqlBinder.cc + orm_lib/src/TransactionImpl.cc + orm_lib/src/RestfulController.cc) if (pg_FOUND OR MySQL_FOUND OR SQLite3_FOUND) set(DROGON_SOURCES - ${DROGON_SOURCES} - orm_lib/src/DbClientManager.cc) + ${DROGON_SOURCES} + orm_lib/src/DbClientManager.cc) else (pg_FOUND OR MySQL_FOUND OR SQLite3_FOUND) set(DROGON_SOURCES ${DROGON_SOURCES} lib/src/DbClientManagerSkipped.cc) endif (pg_FOUND OR MySQL_FOUND OR SQLite3_FOUND) @@ -512,7 +513,8 @@ install(FILES ${DROGON_UTIL_HEADERS} DESTINATION ${INSTALL_INCLUDE_DIR}/drogon/utils) set(DROGON_PLUGIN_HEADERS lib/inc/drogon/plugins/Plugin.h - lib/inc/drogon/plugins/SecureSSLRedirector.h) + lib/inc/drogon/plugins/SecureSSLRedirector.h + lib/inc/drogon/plugins/AccessLogger.h) install(FILES ${DROGON_PLUGIN_HEADERS} DESTINATION ${INSTALL_INCLUDE_DIR}/drogon/plugins) diff --git a/lib/inc/drogon/drogon.h b/lib/inc/drogon/drogon.h index 95fa3b02..945748e6 100644 --- a/lib/inc/drogon/drogon.h +++ b/lib/inc/drogon/drogon.h @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include diff --git a/lib/inc/drogon/plugins/AccessLogger.h b/lib/inc/drogon/plugins/AccessLogger.h new file mode 100644 index 00000000..b6c552d7 --- /dev/null +++ b/lib/inc/drogon/plugins/AccessLogger.h @@ -0,0 +1,178 @@ +/** + * + * AccessLogger.h + * + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace drogon +{ +namespace plugin +{ +/** + * @brief This plugin is used to print all requests to the log. + * + * The json configuration is as follows: + * + * @code + { + "name": "drogon::plugin::AccessLogger", + "dependencies": [], + "config": { + "log_path": "./", + "log_format": "", + "log_file": "access.log", + "log_size_limit": 0, + "use_local_time": true, + "log_index": 0 + } + } + @endcode + * + * log_format: a format string for access logging, there are several + * placeholders that represent particular data. + * $date: the time when the log was printed. + * $request_date: the time when the request was created. + * $request_path|$path: the path of the request. + * $request_query|$query: the query string of the request. + * $request_url|$url: the URL of the request, equals to + * $request_path+"?"+$request_query. + * $remote_addr: the remote address + * $local_addr: the local address + * $request_len|$body_bytes_received: the content length of the request. + * $method: the HTTP method of the request. + * $thread: the current thread number. + * $response_len|$body_bytes_sent: the content length of the response. + * $http_[header_name]: the header of the request. + * $cookie_[cookie_name]: the cookie of the request. + * $upstream_http_[header_name]: the header of the response sent to the + * client. + * $status_code: the status code of the response. + * $status: the status code and string of the response. + * $processing_time: request processing time in seconds with a microseconds + * resolution; time elapsed between the request object was + * created and response object was created. + * @note If the format string is empty or not configured, a default value of + * "$request_date $method $url [$body_bytes_received] ($remote_addr - + * $local_addr) $status $body_bytes_sent $processing_time" is applied. + * + * log_path: Log file path, empty by default,in which case,logs are output to + * the regular log file (or stdout based on the log configuration). + * + * log_file: The access log file name, 'access.log' by default. if the file name + * does not contain a extension, the .log extension is used. + * + * log_size_limit: 0 bytes by default, when the log file size reaches + * "log_size_limit", the log file is switched. Zero value means never switch + * + * log_index: The index of log output, 0 by default. + * + * Enable the plugin by adding the configuration to the list of plugins in the + * configuration file. + * + */ +class DROGON_EXPORT AccessLogger : public drogon::Plugin +{ + public: + AccessLogger() + { + } + void initAndStart(const Json::Value &config) override; + void shutdown() override; + + private: + trantor::AsyncFileLogger asyncFileLogger_; + int logIndex_{0}; + bool useLocalTime_{true}; + using LogFunction = std::function; + std::vector logFunctions_; + void logging(trantor::LogStream &stream, + const drogon::HttpRequestPtr &req, + const drogon::HttpResponsePtr &resp); + void createLogFunctions(std::string format); + LogFunction newLogFunction(const std::string &placeholder); + std::map logFunctionMap_; + //$request_path + static void outputReqPath(trantor::LogStream &, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &); + //$request_query + static void outputReqQuery(trantor::LogStream &, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &); + //$request_url + static void outputReqURL(trantor::LogStream &, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &); + //$date + void outputDate(trantor::LogStream &, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &) const; + //$request_date + void outputReqDate(trantor::LogStream &, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &) const; + //$remote_addr + static void outputRemoteAddr(trantor::LogStream &, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &); + //$local_addr + static void outputLocalAddr(trantor::LogStream &, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &); + //$request_len $body_bytes_received + static void outputReqLength(trantor::LogStream &, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &); + //$response_len $body_bytes_sent + static void outputRespLength(trantor::LogStream &, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &); + //$method + static void outputMethod(trantor::LogStream &, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &); + //$thread + static void outputThreadNumber(trantor::LogStream &, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &); + //$http_[header_name] + static void outputReqHeader(trantor::LogStream &stream, + const drogon::HttpRequestPtr &req, + const std::string &headerName); + //$cookie_[cookie_name] + static void outputReqCookie(trantor::LogStream &stream, + const drogon::HttpRequestPtr &req, + const std::string &cookie); + //$upstream_http_[header_name] + static void outputRespHeader(trantor::LogStream &stream, + const drogon::HttpResponsePtr &resp, + const std::string &headerName); + //$status + static void outputStatusString(trantor::LogStream &, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &); + //$status_code + static void outputStatusCode(trantor::LogStream &, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &); + //$processing_time + static void outputProcessingTime(trantor::LogStream &, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &); + //$upstream_http_content-type $upstream_http_content_type + static void outputRespContentType(trantor::LogStream &, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &); +}; +} // namespace plugin +} // namespace drogon diff --git a/lib/src/AccessLogger.cc b/lib/src/AccessLogger.cc new file mode 100644 index 00000000..b319ff9a --- /dev/null +++ b/lib/src/AccessLogger.cc @@ -0,0 +1,436 @@ +/** + * + * @file AccessLogger.cc + * @author An Tao + * + * Copyright 2018, An Tao. All rights reserved. + * https://github.com/an-tao/drogon + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Drogon + * + */ + +#include "HttpUtils.h" +#include +#include +#include +#include +#ifndef _WIN32 +#include +#include +#else +#include +#endif +#ifdef __FreeBSD__ +#include +#endif + +using namespace drogon; +using namespace drogon::plugin; + +void AccessLogger::initAndStart(const Json::Value &config) +{ + useLocalTime_ = config.get("use_local_time", true).asBool(); + logFunctionMap_ = {{"$request_path", outputReqPath}, + {"$path", outputReqPath}, + {"$date", + [this](trantor::LogStream &stream, + const drogon::HttpRequestPtr &req, + const drogon::HttpResponsePtr &resp) { + outputDate(stream, req, resp); + }}, + {"$request_date", + [this](trantor::LogStream &stream, + const drogon::HttpRequestPtr &req, + const drogon::HttpResponsePtr &resp) { + outputReqDate(stream, req, resp); + }}, + {"$request_query", outputReqQuery}, + {"$request_url", outputReqURL}, + {"$query", outputReqQuery}, + {"$url", outputReqURL}, + {"$remote_addr", outputRemoteAddr}, + {"$local_addr", outputLocalAddr}, + {"$request_len", outputReqLength}, + {"$body_bytes_received", outputReqLength}, + {"$method", outputMethod}, + {"$thread", outputThreadNumber}, + {"$response_len", outputRespLength}, + {"$body_bytes_sent", outputRespLength}, + {"$status", outputStatusString}, + {"$status_code", outputStatusCode}, + {"$processing_time", outputProcessingTime}, + {"$upstream_http_content-type", outputRespContentType}, + {"$upstream_http_content_type", outputRespContentType}}; + auto format = config.get("log_format", "").asString(); + if (format.empty()) + { + format = + "$request_date $method $url [$body_bytes_received] ($remote_addr - " + "$local_addr) $status $body_bytes_sent $processing_time"; + } + createLogFunctions(format); + auto logPath = config.get("log_path", "").asString(); + if (!logPath.empty()) + { + auto fileName = config.get("log_file", "access.log").asString(); + auto extension = std::string(".log"); + auto pos = fileName.rfind('.'); + if (pos != std::string::npos) + { + extension = fileName.substr(pos); + fileName = fileName.substr(0, pos); + } + if (fileName.empty()) + { + fileName = "access"; + } + asyncFileLogger_.setFileName(fileName, extension, logPath); + asyncFileLogger_.startLogging(); + logIndex_ = config.get("log_index", 0).asInt(); + trantor::Logger::setOutputFunction( + [&](const char *msg, const uint64_t len) { + asyncFileLogger_.output(msg, len); + }, + [&]() { asyncFileLogger_.flush(); }, + logIndex_); + auto sizeLimit = config.get("size_limit", 0).asUInt64(); + if (sizeLimit > 0) + { + asyncFileLogger_.setFileSizeLimit(sizeLimit); + } + } + drogon::app().registerPreSendingAdvice( + [this](const drogon::HttpRequestPtr &req, + const drogon::HttpResponsePtr &resp) { + logging(LOG_RAW_TO(logIndex_), req, resp); + }); +} + +void AccessLogger::shutdown() +{ +} + +void AccessLogger::logging(trantor::LogStream &stream, + const drogon::HttpRequestPtr &req, + const drogon::HttpResponsePtr &resp) +{ + for (auto &func : logFunctions_) + { + func(stream, req, resp); + } +} + +void AccessLogger::createLogFunctions(std::string format) +{ + std::string rawString; + while (!format.empty()) + { + LOG_INFO << format; + auto pos = format.find('$'); + if (pos != std::string::npos) + { + rawString += format.substr(0, pos); + + format = format.substr(pos); + std::regex e{"^\\$[a-zA-Z0-9\\-_]+"}; + std::smatch m; + if (std::regex_search(format, m, e)) + { + if (!rawString.empty()) + { + logFunctions_.emplace_back( + [rawString](trantor::LogStream &stream, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &) { + stream << rawString; + }); + rawString.clear(); + } + auto placeholder = m[0]; + logFunctions_.emplace_back(newLogFunction(placeholder)); + format = m.suffix().str(); + } + else + { + rawString += '$'; + format = format.substr(1); + } + } + else + { + rawString += format; + break; + } + } + if (!rawString.empty()) + { + logFunctions_.emplace_back( + [rawString = + std::move(rawString)](trantor::LogStream &stream, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &) { + stream << rawString << "\n"; + }); + } + else + { + logFunctions_.emplace_back( + [](trantor::LogStream &stream, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &) { stream << "\n"; }); + } +} + +AccessLogger::LogFunction AccessLogger::newLogFunction( + const std::string &placeholder) +{ + auto iter = logFunctionMap_.find(placeholder); + if (iter != logFunctionMap_.end()) + { + return iter->second; + } + if (placeholder.find("$http_") == 0 && placeholder.size() > 6) + { + auto headerName = placeholder.substr(6); + return [headerName = + std::move(headerName)](trantor::LogStream &stream, + const drogon::HttpRequestPtr &req, + const drogon::HttpResponsePtr &) { + outputReqHeader(stream, req, headerName); + }; + } + if (placeholder.find("$cookie_") == 0 && placeholder.size() > 8) + { + auto cookieName = placeholder.substr(8); + return [cookieName = + std::move(cookieName)](trantor::LogStream &stream, + const drogon::HttpRequestPtr &req, + const drogon::HttpResponsePtr &) { + outputReqCookie(stream, req, cookieName); + }; + } + if (placeholder.find("$upstream_http_") == 0 && placeholder.size() > 15) + { + auto headerName = placeholder.substr(15); + return [headerName = std::move( + headerName)](trantor::LogStream &stream, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &resp) { + outputRespHeader(stream, resp, headerName); + }; + } + return [placeholder](trantor::LogStream &stream, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &) { + stream << placeholder; + }; +} + +void AccessLogger::outputReqPath(trantor::LogStream &stream, + const drogon::HttpRequestPtr &req, + const drogon::HttpResponsePtr &) +{ + stream << req->path(); +} + +void AccessLogger::outputDate(trantor::LogStream &stream, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &) const +{ + if (useLocalTime_) + { + stream << trantor::Date::now().toFormattedStringLocal(true); + } + else + { + stream << trantor::Date::now().toFormattedString(true); + } +} + +void AccessLogger::outputReqDate(trantor::LogStream &stream, + const drogon::HttpRequestPtr &req, + const drogon::HttpResponsePtr &) const +{ + if (useLocalTime_) + { + stream << req->creationDate().toFormattedStringLocal(true); + } + else + { + stream << req->creationDate().toFormattedString(true); + } +} + +//$request_query +void AccessLogger::outputReqQuery(trantor::LogStream &stream, + const drogon::HttpRequestPtr &req, + const drogon::HttpResponsePtr &) +{ + stream << req->query(); +} +//$request_url +void AccessLogger::outputReqURL(trantor::LogStream &stream, + const drogon::HttpRequestPtr &req, + const drogon::HttpResponsePtr &) +{ + auto &query = req->query(); + if (query.empty()) + { + stream << req->path(); + } + else + { + stream << req->path() << '?' << query; + } +} + +void AccessLogger::outputRemoteAddr(trantor::LogStream &stream, + const drogon::HttpRequestPtr &req, + const drogon::HttpResponsePtr &) +{ + stream << req->peerAddr().toIpPort(); +} + +void AccessLogger::outputLocalAddr(trantor::LogStream &stream, + const drogon::HttpRequestPtr &req, + const drogon::HttpResponsePtr &) +{ + stream << req->localAddr().toIpPort(); +} + +void AccessLogger::outputReqLength(trantor::LogStream &stream, + const drogon::HttpRequestPtr &req, + const drogon::HttpResponsePtr &) +{ + stream << req->body().length(); +} + +void AccessLogger::outputRespLength(trantor::LogStream &stream, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &resp) +{ + stream << resp->body().length(); +} +void AccessLogger::outputMethod(trantor::LogStream &stream, + const drogon::HttpRequestPtr &req, + const drogon::HttpResponsePtr &) +{ + stream << req->methodString(); +} + +void AccessLogger::outputThreadNumber(trantor::LogStream &stream, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &) +{ +#ifdef __linux__ + static thread_local pid_t threadId_{0}; +#else + static thread_local uint64_t threadId_{0}; +#endif +#ifdef __linux__ + if (threadId_ == 0) + threadId_ = static_cast(::syscall(SYS_gettid)); +#elif defined __FreeBSD__ + if (threadId_ == 0) + { + threadId_ = pthread_getthreadid_np(); + } +#elif defined __OpenBSD__ + if (threadId_ == 0) + { + threadId_ = getthrid(); + } +#elif defined _WIN32 + if (threadId_ == 0) + { + std::stringstream ss; + ss << std::this_thread::get_id(); + threadId_ = std::stoull(ss.str()); + } +#else + if (threadId_ == 0) + { + pthread_threadid_np(NULL, &threadId_); + } +#endif + stream << threadId_; +} + +//$http_[header_name] +void AccessLogger::outputReqHeader(trantor::LogStream &stream, + const drogon::HttpRequestPtr &req, + const std::string &headerName) +{ + stream << headerName << ": " << req->getHeader(headerName); +} + +//$cookie_[cookie_name] +void AccessLogger::outputReqCookie(trantor::LogStream &stream, + const drogon::HttpRequestPtr &req, + const std::string &cookie) +{ + stream << "(cookie)" << cookie << "=" << req->getCookie(cookie); +} + +//$upstream_http_[header_name] +void AccessLogger::outputRespHeader(trantor::LogStream &stream, + const drogon::HttpResponsePtr &resp, + const std::string &headerName) +{ + stream << headerName << ": " << resp->getHeader(headerName); +} + +//$status +void AccessLogger::outputStatusString(trantor::LogStream &stream, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &resp) +{ + int code = resp->getStatusCode(); + stream << code << " " << statusCodeToString(code); +} +//$status_code +void AccessLogger::outputStatusCode(trantor::LogStream &stream, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &resp) +{ + stream << resp->getStatusCode(); +} +//$processing_time +void AccessLogger::outputProcessingTime(trantor::LogStream &stream, + const drogon::HttpRequestPtr &req, + const drogon::HttpResponsePtr &) +{ + auto start = req->creationDate(); + auto end = trantor::Date::now(); + auto duration = + end.microSecondsSinceEpoch() - start.microSecondsSinceEpoch(); + auto seconds = static_cast(duration) / 1000000.0; + stream << seconds; +} + +//$upstream_http_content-type $upstream_http_content_type +void AccessLogger::outputRespContentType(trantor::LogStream &stream, + const drogon::HttpRequestPtr &, + const drogon::HttpResponsePtr &resp) +{ + auto typeStr = webContentTypeToString(resp->contentType()); + if (typeStr.empty()) + { + stream << "content-type: "; + } + else + { + if (typeStr.size() >= 2 && typeStr[typeStr.size() - 1] == '\n' && + typeStr[typeStr.size() - 2] == '\r') + { + stream << string_view{typeStr.data(), typeStr.size() - 2}; + } + else + { + stream << typeStr; + } + } +} \ No newline at end of file diff --git a/lib/src/HttpRequestImpl.cc b/lib/src/HttpRequestImpl.cc index 1347071b..0bd4a369 100644 --- a/lib/src/HttpRequestImpl.cc +++ b/lib/src/HttpRequestImpl.cc @@ -350,7 +350,7 @@ void HttpRequestImpl::appendToBuffer(trantor::MsgBuffer *output) const for (auto it = cookies_.begin(); it != cookies_.end(); ++it) { output->append(it->first); - output->append("= "); + output->append("="); output->append(it->second); output->append(";"); } @@ -399,6 +399,11 @@ void HttpRequestImpl::addHeader(const char *start, ++cpos; cookie_name = cookie_name.substr(cpos); std::string cookie_value = coo.substr(epos + 1); + cpos = 0; + while (cpos < cookie_value.length() && + isspace(cookie_value[cpos])) + ++cpos; + cookie_value = cookie_value.substr(cpos); cookies_[std::move(cookie_name)] = std::move(cookie_value); } value = value.substr(pos + 1); @@ -416,6 +421,11 @@ void HttpRequestImpl::addHeader(const char *start, ++cpos; cookie_name = cookie_name.substr(cpos); std::string cookie_value = coo.substr(epos + 1); + cpos = 0; + while (cpos < cookie_value.length() && + isspace(cookie_value[cpos])) + ++cpos; + cookie_value = cookie_value.substr(cpos); cookies_[std::move(cookie_name)] = std::move(cookie_value); } }