diff --git a/config.example.json b/config.example.json index 41b6c94c..12e166ca 100644 --- a/config.example.json +++ b/config.example.json @@ -226,5 +226,9 @@ } }], //custom_config: custom configuration for users. This object can be get by the app().getCustomConfig() method. - "custom_config": {} + "custom_config": { + "realm" : "drogonRealm", + "opaque" : "drogonOpaque", + "credentials" : [ {"user" : "drogon", "password": "dr0g0n"} ] + } } \ No newline at end of file diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index cd1583f0..17554c4c 100755 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -31,6 +31,7 @@ set(simple_example_sources simple_example/api_Attachment.cc simple_example/api_v1_ApiTest.cc simple_example/TimeFilter.cc + simple_example/DigestAuthFilter.cc simple_example/main.cc) add_executable(webapp ${simple_example_sources} ${VIEWSRC}) diff --git a/examples/simple_example/DigestAuthFilter.cc b/examples/simple_example/DigestAuthFilter.cc new file mode 100644 index 00000000..ea08fc57 --- /dev/null +++ b/examples/simple_example/DigestAuthFilter.cc @@ -0,0 +1,215 @@ +#include "DigestAuthFilter.h" + +#include +#include +#include +#include + +std::string method2String(HttpMethod m) +{ + switch (m) + { + case Get: + return "GET"; + case Post: + return "POST"; + case Head: + return "HEAD"; + case Put: + return "PUT"; + case Delete: + return "DELETE"; + case Options: + return "OPTIONS"; + default: + return "INVALID"; + } +} + +std::string toLower(const std::string &in) +{ + std::string out = in; + std::transform(out.begin(), out.end(), out.begin(), [](unsigned char c) { + return std::tolower(c); + }); + return out; +} + +bool DigestAuthFilter::isEndOfAttributeName(size_t pos, + size_t len, + const char *data) +{ + if (pos >= len) + return true; + if (isspace(static_cast(data[pos]))) + return true; + // The reason for this complexity is that some attributes may contain + // trailing equal signs (like base64 tokens in Negotiate auth headers) + if ((pos + 1 < len) && (data[pos] == '=') && + !isspace(static_cast(data[pos + 1])) && + (data[pos + 1] != '=')) + { + return true; + } + return false; +} + +void DigestAuthFilter::httpParseAttributes(const char *data, + size_t len, + HttpAttributeList &attributes) +{ + size_t pos = 0; + while (true) + { + // Skip leading whitespace + while ((pos < len) && isspace(static_cast(data[pos]))) + { + ++pos; + } + + // End of attributes? + if (pos >= len) + return; + + // Find end of attribute name + size_t start = pos; + while (!isEndOfAttributeName(pos, len, data)) + { + ++pos; + } + + HttpAttribute attribute; + attribute.first.assign(data + start, data + pos); + + // Attribute has value? + if ((pos < len) && (data[pos] == '=')) + { + ++pos; // Skip '=' + // Check if quoted value + if ((pos < len) && (data[pos] == '"')) + { + while (++pos < len) + { + if (data[pos] == '"') + { + ++pos; + break; + } + if ((data[pos] == '\\') && (pos + 1 < len)) + ++pos; + attribute.second.append(1, data[pos]); + } + } + else + { + while ((pos < len) && + !isspace(static_cast(data[pos])) && + (data[pos] != ',')) + { + attribute.second.append(1, data[pos++]); + } + } + } + + attributes.push_back(attribute); + if ((pos < len) && (data[pos] == ',')) + ++pos; // Skip ',' + } +} + +bool DigestAuthFilter::httpHasAttribute(const HttpAttributeList &attributes, + const std::string &name, + std::string *value) +{ + for (HttpAttributeList::const_iterator it = attributes.begin(); + it != attributes.end(); + ++it) + { + if (it->first == name) + { + if (value) + { + *value = it->second; + } + return true; + } + } + return false; +} + +DigestAuthFilter::DigestAuthFilter( + const std::map &credentials, + const std::string &realm, + const std::string &opaque) + : credentials(credentials), realm(realm), opaque(opaque) +{ +} + +void DigestAuthFilter::doFilter(const HttpRequestPtr &req, + FilterCallback &&cb, + FilterChainCallback &&ccb) +{ + if (!req->session()) + { + // no session support by framework,pls enable session + auto resp = HttpResponse::newNotFoundResponse(); + cb(resp); + return; + } + + auto auth_header = req->getHeader("Authorization"); + if (!auth_header.empty()) + { + HttpAttributeList att_list; + httpParseAttributes(auth_header.c_str(), auth_header.size(), att_list); + std::string username, realm, nonce, uri, opaque, response; + if (httpHasAttribute(att_list, "username", &username) && + httpHasAttribute(att_list, "realm", &realm) && + httpHasAttribute(att_list, "nonce", &nonce) && + httpHasAttribute(att_list, "uri", &uri) && + httpHasAttribute(att_list, "opaque", &opaque) && + httpHasAttribute(att_list, "response", &response)) + { + if (credentials.find(username) != credentials.end()) + { + std::string A1 = + username + ":" + realm + ":" + credentials.at(username); + std::string A2 = method2String(req->getMethod()) + ":" + uri; + std::string A1_middle_A2 = toLower(utils::getMd5(A1)) + ":" + + nonce + ":" + + toLower(utils::getMd5(A2)); + std::string calculated_response = + toLower(utils::getMd5(A1_middle_A2)); + if (response == calculated_response) + { + // Passed + ccb(); + return; + } + else + { + LOG_DEBUG << "invalid response " << response + << ", calculated " << calculated_response; + } + } + else + { + LOG_DEBUG << "invalid username " << username; + } + } + else + { + LOG_DEBUG << "missing attributes in WWW-Authenticate header" + << auth_header; + } + } + // not Passed + auto resp = HttpResponse::newHttpResponse(); + resp->setStatusCode(k401Unauthorized); + resp->addHeader("WWW-Authenticate", + " Digest realm=\"" + realm + "\", nonce=\"" + + toLower(utils::getMd5(std::to_string(time(0)))) + + "\", opaque=\"" + opaque + "\""); + cb(resp); + return; +} \ No newline at end of file diff --git a/examples/simple_example/DigestAuthFilter.h b/examples/simple_example/DigestAuthFilter.h new file mode 100644 index 00000000..03df6349 --- /dev/null +++ b/examples/simple_example/DigestAuthFilter.h @@ -0,0 +1,33 @@ +#pragma once + +#include +using namespace drogon; + +typedef std::pair HttpAttribute; +typedef std::vector HttpAttributeList; +typedef std::map + CredentialsMap; + +class DigestAuthFilter : public drogon::HttpFilter +{ + const std::map credentials; + const std::string realm; + const std::string opaque; + + static bool isEndOfAttributeName(size_t pos, size_t len, const char *data); + static void httpParseAttributes(const char *data, + size_t len, + HttpAttributeList &attributes); + + static bool httpHasAttribute(const HttpAttributeList &attributes, + const std::string &name, + std::string *value); + + public: + explicit DigestAuthFilter(const CredentialsMap &credentials, + const std::string &realm, + const std::string &opaque); + virtual void doFilter(const HttpRequestPtr &req, + FilterCallback &&cb, + FilterChainCallback &&ccb) override; +}; \ No newline at end of file diff --git a/examples/simple_example/main.cc b/examples/simple_example/main.cc index 7e4b8c86..dbb9ef93 100644 --- a/examples/simple_example/main.cc +++ b/examples/simple_example/main.cc @@ -1,6 +1,7 @@ #include "CustomCtrl.h" #include "CustomHeaderFilter.h" +#include "DigestAuthFilter.h" #include #include #include @@ -69,6 +70,22 @@ class B : public DrObjectBase callback(res); } }; + +class C : public drogon::HttpController +{ + public: + METHOD_LIST_BEGIN + ADD_METHOD_TO(C::priv, "/priv/resource", Get, "DigestAuthFilter"); + METHOD_LIST_END + void priv(const HttpRequestPtr &req, + std::function &&callback) const + { + auto resp = HttpResponse::newHttpResponse(); + resp->setBody("

private content, only for authenticated users

"); + callback(resp); + } +}; + namespace api { namespace v1 @@ -192,6 +209,9 @@ int main() app().setDocumentRoot("./"); app().enableSession(60); + std::map config_credentials; + std::string realm("drogonRealm"); + std::string opaque("drogonOpaque"); // Load configuration app().loadConfigFile("config.example.json"); auto &json = app().getCustomConfig(); @@ -199,6 +219,27 @@ int main() { std::cout << "empty custom config!" << std::endl; } + else + { + if (!json["realm"].empty()) + { + realm = json["realm"].asString(); + } + if (!json["opaque"].empty()) + { + opaque = json["opaque"].asString(); + } + for (auto &&i : json["credentials"]) + { + config_credentials[i["user"].asString()] = i["password"].asString(); + } + } + + // Install Digest Authentication Filter using custom config credentials, + // used by C HttpController (/C/priv/resource) + auto auth_filter = + std::make_shared(config_credentials, realm, opaque); + app().registerFilter(auth_filter); // Install custom controller auto ctrlPtr = std::make_shared("Hi"); diff --git a/lib/inc/drogon/utils/Utilities.h b/lib/inc/drogon/utils/Utilities.h index ec1747d2..b7075636 100644 --- a/lib/inc/drogon/utils/Utilities.h +++ b/lib/inc/drogon/utils/Utilities.h @@ -97,6 +97,9 @@ inline std::string urlDecode(const string_view &szToDecode) std::string urlEncode(const std::string &); std::string urlEncodeComponent(const std::string &); +/// Get the MD5 digest of a string. +std::string getMd5(const std::string &originalString); + /// Commpress or decompress data using gzip lib. /** * @param data the input data diff --git a/lib/src/MultiPart.cc b/lib/src/MultiPart.cc index 77bc6337..ff90e1dc 100644 --- a/lib/src/MultiPart.cc +++ b/lib/src/MultiPart.cc @@ -18,11 +18,6 @@ #include #include #include -#ifdef OpenSSL_FOUND -#include -#else -#include "ssl_funcs/Md5.h" -#endif #include #include #include @@ -226,14 +221,5 @@ int HttpFile::saveTo(const std::string &pathAndFilename) const } std::string HttpFile::getMd5() const { -#ifdef OpenSSL_FOUND - MD5_CTX c; - unsigned char md5[16] = {0}; - MD5_Init(&c); - MD5_Update(&c, fileContent_.c_str(), fileContent_.size()); - MD5_Final(md5, &c); - return utils::binaryStringToHex(md5, 16); -#else - return Md5Encode::encode(fileContent_); -#endif + return utils::getMd5(fileContent_); } diff --git a/lib/src/Utilities.cc b/lib/src/Utilities.cc index 18696358..e5819ce1 100644 --- a/lib/src/Utilities.cc +++ b/lib/src/Utilities.cc @@ -14,6 +14,12 @@ #include #include +#include +#ifdef OpenSSL_FOUND +#include +#else +#include "ssl_funcs/Md5.h" +#endif #ifdef USE_BROTLI #include #include @@ -1138,5 +1144,19 @@ std::string brotliDecompress(const char *data, const size_t ndata) } #endif +std::string getMd5(const std::string &originalString) +{ +#ifdef OpenSSL_FOUND + MD5_CTX c; + unsigned char md5[16] = {0}; + MD5_Init(&c); + MD5_Update(&c, originalString.c_str(), originalString.size()); + MD5_Final(md5, &c); + return utils::binaryStringToHex(md5, 16); +#else + return Md5Encode::encode(originalString); +#endif +} + } // namespace utils } // namespace drogon diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index 55adb082..abda59d3 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -1,7 +1,7 @@ add_executable(drogon_msgbuffer_unittest MsgBufferUnittest.cpp) add_executable(drobject_unittest DrObjectUnittest.cpp) add_executable(gzip_unittest GzipUnittest.cpp) -add_executable(md5_unittest MD5Unittest.cpp ../lib/src/ssl_funcs/Md5.cc) +add_executable(md5_unittest MD5Unittest.cpp) add_executable(sha1_unittest SHA1Unittest.cpp ../lib/src/ssl_funcs/Sha1.cc) add_executable(ostringstream_unittest OStringStreamUnitttest.cpp) add_executable(base64_unittest Base64Unittest.cpp) diff --git a/unittest/MD5Unittest.cpp b/unittest/MD5Unittest.cpp index cafd2123..06dd2bac 100644 --- a/unittest/MD5Unittest.cpp +++ b/unittest/MD5Unittest.cpp @@ -1,14 +1,15 @@ -#include "../lib/src/ssl_funcs/Md5.h" +#include #include #include TEST(Md5Test, md5) { - // EXPECT_EQ(Md5Encode::encode("123456789012345678901234567890123456789012345" - // "678901234567890123456789012345678901234567890" - // "1234567890"), - // "49CB3608E2B33FAD6B65DF8CB8F49668"); - EXPECT_EQ(Md5Encode::encode("1"), "C4CA4238A0B923820DCC509A6F75849B"); + EXPECT_EQ(drogon::utils::getMd5( + "123456789012345678901234567890123456789012345" + "678901234567890123456789012345678901234567890" + "1234567890"), + "49CB3608E2B33FAD6B65DF8CB8F49668"); + EXPECT_EQ(drogon::utils::getMd5("1"), "C4CA4238A0B923820DCC509A6F75849B"); } int main(int argc, char **argv)