| 1 | // |
| 2 | // Database.cpp |
| 3 | // |
| 4 | // Library: MongoDB |
| 5 | // Package: MongoDB |
| 6 | // Module: Database |
| 7 | // |
| 8 | // Copyright (c) 2012, Applied Informatics Software Engineering GmbH. |
| 9 | // and Contributors. |
| 10 | // |
| 11 | // SPDX-License-Identifier: BSL-1.0 |
| 12 | // |
| 13 | |
| 14 | |
| 15 | #include "Poco/MongoDB/Database.h" |
| 16 | #include "Poco/MongoDB/Binary.h" |
| 17 | #include "Poco/MD5Engine.h" |
| 18 | #include "Poco/SHA1Engine.h" |
| 19 | #include "Poco/PBKDF2Engine.h" |
| 20 | #include "Poco/HMACEngine.h" |
| 21 | #include "Poco/Base64Decoder.h" |
| 22 | #include "Poco/MemoryStream.h" |
| 23 | #include "Poco/StreamCopier.h" |
| 24 | #include "Poco/Exception.h" |
| 25 | #include "Poco/RandomStream.h" |
| 26 | #include "Poco/Random.h" |
| 27 | #include "Poco/Format.h" |
| 28 | #include "Poco/NumberParser.h" |
| 29 | #include <sstream> |
| 30 | #include <map> |
| 31 | |
| 32 | |
| 33 | namespace Poco { |
| 34 | namespace MongoDB { |
| 35 | |
| 36 | |
| 37 | const std::string Database::AUTH_MONGODB_CR("MONGODB-CR" ); |
| 38 | const std::string Database::AUTH_SCRAM_SHA1("SCRAM-SHA-1" ); |
| 39 | |
| 40 | |
| 41 | namespace |
| 42 | { |
| 43 | std::map<std::string, std::string> parseKeyValueList(const std::string& str) |
| 44 | { |
| 45 | std::map<std::string, std::string> kvm; |
| 46 | std::string::const_iterator it = str.begin(); |
| 47 | std::string::const_iterator end = str.end(); |
| 48 | while (it != end) |
| 49 | { |
| 50 | std::string k; |
| 51 | std::string v; |
| 52 | while (it != end && *it != '=') k += *it++; |
| 53 | if (it != end) ++it; |
| 54 | while (it != end && *it != ',') v += *it++; |
| 55 | if (it != end) ++it; |
| 56 | kvm[k] = v; |
| 57 | } |
| 58 | return kvm; |
| 59 | } |
| 60 | |
| 61 | std::string decodeBase64(const std::string& base64) |
| 62 | { |
| 63 | Poco::MemoryInputStream istr(base64.data(), base64.size()); |
| 64 | Poco::Base64Decoder decoder(istr); |
| 65 | std::string result; |
| 66 | Poco::StreamCopier::copyToString(decoder, result); |
| 67 | return result; |
| 68 | } |
| 69 | |
| 70 | std::string encodeBase64(const std::string& data) |
| 71 | { |
| 72 | std::ostringstream ostr; |
| 73 | Poco::Base64Encoder encoder(ostr); |
| 74 | encoder.rdbuf()->setLineLength(0); |
| 75 | encoder << data; |
| 76 | encoder.close(); |
| 77 | return ostr.str(); |
| 78 | } |
| 79 | |
| 80 | std::string digestToBinaryString(Poco::DigestEngine& engine) |
| 81 | { |
| 82 | Poco::DigestEngine::Digest d = engine.digest(); |
| 83 | return std::string(reinterpret_cast<const char*>(&d[0]), d.size()); |
| 84 | } |
| 85 | |
| 86 | std::string digestToHexString(Poco::DigestEngine& engine) |
| 87 | { |
| 88 | Poco::DigestEngine::Digest d = engine.digest(); |
| 89 | return Poco::DigestEngine::digestToHex(d); |
| 90 | } |
| 91 | |
| 92 | std::string digestToBase64(Poco::DigestEngine& engine) |
| 93 | { |
| 94 | return encodeBase64(digestToBinaryString(engine)); |
| 95 | } |
| 96 | |
| 97 | std::string hashCredentials(const std::string& username, const std::string& password) |
| 98 | { |
| 99 | Poco::MD5Engine md5; |
| 100 | md5.update(username); |
| 101 | md5.update(std::string(":mongo:" )); |
| 102 | md5.update(password); |
| 103 | return digestToHexString(md5); |
| 104 | } |
| 105 | |
| 106 | std::string createNonce() |
| 107 | { |
| 108 | Poco::MD5Engine md5; |
| 109 | Poco::RandomInputStream randomStream; |
| 110 | Poco::Random random; |
| 111 | for (int i = 0; i < 4; i++) |
| 112 | { |
| 113 | md5.update(randomStream.get()); |
| 114 | md5.update(random.nextChar()); |
| 115 | } |
| 116 | return digestToHexString(md5); |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | |
| 121 | Database::Database(const std::string& db): |
| 122 | _dbname(db) |
| 123 | { |
| 124 | } |
| 125 | |
| 126 | |
| 127 | Database::~Database() |
| 128 | { |
| 129 | } |
| 130 | |
| 131 | |
| 132 | bool Database::authenticate(Connection& connection, const std::string& username, const std::string& password, const std::string& method) |
| 133 | { |
| 134 | if (username.empty()) throw Poco::InvalidArgumentException("empty username" ); |
| 135 | if (password.empty()) throw Poco::InvalidArgumentException("empty password" ); |
| 136 | |
| 137 | if (method == AUTH_MONGODB_CR) |
| 138 | return authCR(connection, username, password); |
| 139 | else if (method == AUTH_SCRAM_SHA1) |
| 140 | return authSCRAM(connection, username, password); |
| 141 | else |
| 142 | throw Poco::InvalidArgumentException("authentication method" , method); |
| 143 | } |
| 144 | |
| 145 | |
| 146 | bool Database::authCR(Connection& connection, const std::string& username, const std::string& password) |
| 147 | { |
| 148 | std::string nonce; |
| 149 | Poco::SharedPtr<QueryRequest> pCommand = createCommand(); |
| 150 | pCommand->selector().add<Poco::Int32>("getnonce" , 1); |
| 151 | |
| 152 | ResponseMessage response; |
| 153 | connection.sendRequest(*pCommand, response); |
| 154 | if (response.documents().size() > 0) |
| 155 | { |
| 156 | Document::Ptr pDoc = response.documents()[0]; |
| 157 | if (pDoc->getInteger("ok" ) != 1) return false; |
| 158 | nonce = pDoc->get<std::string>("nonce" , "" ); |
| 159 | if (nonce.empty()) throw Poco::ProtocolException("no nonce received" ); |
| 160 | } |
| 161 | else throw Poco::ProtocolException("empty response for getnonce" ); |
| 162 | |
| 163 | std::string credsDigest = hashCredentials(username, password); |
| 164 | |
| 165 | Poco::MD5Engine md5; |
| 166 | md5.update(nonce); |
| 167 | md5.update(username); |
| 168 | md5.update(credsDigest); |
| 169 | std::string key = digestToHexString(md5); |
| 170 | |
| 171 | pCommand = createCommand(); |
| 172 | pCommand->selector() |
| 173 | .add<Poco::Int32>("authenticate" , 1) |
| 174 | .add<std::string>("user" , username) |
| 175 | .add<std::string>("nonce" , nonce) |
| 176 | .add<std::string>("key" , key); |
| 177 | |
| 178 | connection.sendRequest(*pCommand, response); |
| 179 | if (response.documents().size() > 0) |
| 180 | { |
| 181 | Document::Ptr pDoc = response.documents()[0]; |
| 182 | return pDoc->getInteger("ok" ) == 1; |
| 183 | } |
| 184 | else throw Poco::ProtocolException("empty response for authenticate" ); |
| 185 | } |
| 186 | |
| 187 | |
| 188 | bool Database::authSCRAM(Connection& connection, const std::string& username, const std::string& password) |
| 189 | { |
| 190 | std::string clientNonce(createNonce()); |
| 191 | std::string clientFirstMsg = Poco::format("n=%s,r=%s" , username, clientNonce); |
| 192 | |
| 193 | Poco::SharedPtr<QueryRequest> pCommand = createCommand(); |
| 194 | pCommand->selector() |
| 195 | .add<Poco::Int32>("saslStart" , 1) |
| 196 | .add<std::string>("mechanism" , AUTH_SCRAM_SHA1) |
| 197 | .add<Binary::Ptr>("payload" , new Binary(Poco::format("n,,%s" , clientFirstMsg))) |
| 198 | .add<bool>("authAuthorize" , true); |
| 199 | |
| 200 | ResponseMessage response; |
| 201 | connection.sendRequest(*pCommand, response); |
| 202 | |
| 203 | Int32 conversationId = 0; |
| 204 | std::string serverFirstMsg; |
| 205 | |
| 206 | if (response.documents().size() > 0) |
| 207 | { |
| 208 | Document::Ptr pDoc = response.documents()[0]; |
| 209 | if (pDoc->getInteger("ok" ) == 1) |
| 210 | { |
| 211 | Binary::Ptr pPayload = pDoc->get<Binary::Ptr>("payload" ); |
| 212 | serverFirstMsg = pPayload->toRawString(); |
| 213 | conversationId = pDoc->get<Int32>("conversationId" ); |
| 214 | } |
| 215 | else return false; |
| 216 | } |
| 217 | else throw Poco::ProtocolException("empty response for saslStart" ); |
| 218 | |
| 219 | std::map<std::string, std::string> kvm = parseKeyValueList(serverFirstMsg); |
| 220 | const std::string serverNonce = kvm["r" ]; |
| 221 | const std::string salt = decodeBase64(kvm["s" ]); |
| 222 | const unsigned iterations = Poco::NumberParser::parseUnsigned(kvm["i" ]); |
| 223 | const Poco::UInt32 dkLen = 20; |
| 224 | |
| 225 | std::string hashedPassword = hashCredentials(username, password); |
| 226 | |
| 227 | Poco::PBKDF2Engine<Poco::HMACEngine<Poco::SHA1Engine> > pbkdf2(salt, iterations, dkLen); |
| 228 | pbkdf2.update(hashedPassword); |
| 229 | std::string saltedPassword = digestToBinaryString(pbkdf2); |
| 230 | |
| 231 | std::string clientFinalNoProof = Poco::format("c=biws,r=%s" , serverNonce); |
| 232 | std::string authMessage = Poco::format("%s,%s,%s" , clientFirstMsg, serverFirstMsg, clientFinalNoProof); |
| 233 | |
| 234 | Poco::HMACEngine<Poco::SHA1Engine> hmacKey(saltedPassword); |
| 235 | hmacKey.update(std::string("Client Key" )); |
| 236 | std::string clientKey = digestToBinaryString(hmacKey); |
| 237 | |
| 238 | Poco::SHA1Engine sha1; |
| 239 | sha1.update(clientKey); |
| 240 | std::string storedKey = digestToBinaryString(sha1); |
| 241 | |
| 242 | Poco::HMACEngine<Poco::SHA1Engine> hmacSig(storedKey); |
| 243 | hmacSig.update(authMessage); |
| 244 | std::string clientSignature = digestToBinaryString(hmacSig); |
| 245 | |
| 246 | std::string clientProof(clientKey); |
| 247 | for (std::size_t i = 0; i < clientProof.size(); i++) |
| 248 | { |
| 249 | clientProof[i] ^= clientSignature[i]; |
| 250 | } |
| 251 | |
| 252 | std::string clientFinal = Poco::format("%s,p=%s" , clientFinalNoProof, encodeBase64(clientProof)); |
| 253 | |
| 254 | pCommand = createCommand(); |
| 255 | pCommand->selector() |
| 256 | .add<Poco::Int32>("saslContinue" , 1) |
| 257 | .add<Poco::Int32>("conversationId" , conversationId) |
| 258 | .add<Binary::Ptr>("payload" , new Binary(clientFinal)); |
| 259 | |
| 260 | std::string serverSecondMsg; |
| 261 | connection.sendRequest(*pCommand, response); |
| 262 | if (response.documents().size() > 0) |
| 263 | { |
| 264 | Document::Ptr pDoc = response.documents()[0]; |
| 265 | if (pDoc->getInteger("ok" ) == 1) |
| 266 | { |
| 267 | Binary::Ptr pPayload = pDoc->get<Binary::Ptr>("payload" ); |
| 268 | serverSecondMsg = pPayload->toRawString(); |
| 269 | } |
| 270 | else return false; |
| 271 | } |
| 272 | else throw Poco::ProtocolException("empty response for saslContinue" ); |
| 273 | |
| 274 | Poco::HMACEngine<Poco::SHA1Engine> hmacSKey(saltedPassword); |
| 275 | hmacSKey.update(std::string("Server Key" )); |
| 276 | std::string serverKey = digestToBinaryString(hmacSKey); |
| 277 | |
| 278 | Poco::HMACEngine<Poco::SHA1Engine> hmacSSig(serverKey); |
| 279 | hmacSSig.update(authMessage); |
| 280 | std::string serverSignature = digestToBase64(hmacSSig); |
| 281 | |
| 282 | kvm = parseKeyValueList(serverSecondMsg); |
| 283 | std::string serverSignatureReceived = kvm["v" ]; |
| 284 | |
| 285 | if (serverSignature != serverSignatureReceived) |
| 286 | throw Poco::ProtocolException("server signature verification failed" ); |
| 287 | |
| 288 | pCommand = createCommand(); |
| 289 | pCommand->selector() |
| 290 | .add<Poco::Int32>("saslContinue" , 1) |
| 291 | .add<Poco::Int32>("conversationId" , conversationId) |
| 292 | .add<Binary::Ptr>("payload" , new Binary); |
| 293 | |
| 294 | connection.sendRequest(*pCommand, response); |
| 295 | if (response.documents().size() > 0) |
| 296 | { |
| 297 | Document::Ptr pDoc = response.documents()[0]; |
| 298 | return pDoc->getInteger("ok" ) == 1; |
| 299 | } |
| 300 | else throw Poco::ProtocolException("empty response for saslContinue" ); |
| 301 | } |
| 302 | |
| 303 | |
| 304 | Int64 Database::count(Connection& connection, const std::string& collectionName) const |
| 305 | { |
| 306 | Poco::SharedPtr<Poco::MongoDB::QueryRequest> countRequest = createCountRequest(collectionName); |
| 307 | |
| 308 | Poco::MongoDB::ResponseMessage response; |
| 309 | connection.sendRequest(*countRequest, response); |
| 310 | |
| 311 | if (response.documents().size() > 0) |
| 312 | { |
| 313 | Poco::MongoDB::Document::Ptr doc = response.documents()[0]; |
| 314 | return doc->getInteger("n" ); |
| 315 | } |
| 316 | |
| 317 | return -1; |
| 318 | } |
| 319 | |
| 320 | |
| 321 | Poco::MongoDB::Document::Ptr Database::ensureIndex(Connection& connection, const std::string& collection, const std::string& indexName, Poco::MongoDB::Document::Ptr keys, bool unique, bool background, int version, int ttl) |
| 322 | { |
| 323 | Poco::MongoDB::Document::Ptr index = new Poco::MongoDB::Document(); |
| 324 | index->add("ns" , _dbname + "." + collection); |
| 325 | index->add("name" , indexName); |
| 326 | index->add("key" , keys); |
| 327 | |
| 328 | if (version > 0) |
| 329 | { |
| 330 | index->add("version" , version); |
| 331 | } |
| 332 | |
| 333 | if (unique) |
| 334 | { |
| 335 | index->add("unique" , true); |
| 336 | } |
| 337 | |
| 338 | if (background) |
| 339 | { |
| 340 | index->add("background" , true); |
| 341 | } |
| 342 | |
| 343 | if (ttl > 0) |
| 344 | { |
| 345 | index->add("expireAfterSeconds" , ttl); |
| 346 | } |
| 347 | |
| 348 | Poco::SharedPtr<Poco::MongoDB::InsertRequest> insertRequest = createInsertRequest("system.indexes" ); |
| 349 | insertRequest->documents().push_back(index); |
| 350 | connection.sendRequest(*insertRequest); |
| 351 | |
| 352 | return getLastErrorDoc(connection); |
| 353 | } |
| 354 | |
| 355 | |
| 356 | Document::Ptr Database::getLastErrorDoc(Connection& connection) const |
| 357 | { |
| 358 | Document::Ptr errorDoc; |
| 359 | |
| 360 | Poco::SharedPtr<Poco::MongoDB::QueryRequest> request = createQueryRequest("$cmd" ); |
| 361 | request->setNumberToReturn(1); |
| 362 | request->selector().add("getLastError" , 1); |
| 363 | |
| 364 | Poco::MongoDB::ResponseMessage response; |
| 365 | connection.sendRequest(*request, response); |
| 366 | |
| 367 | if (response.documents().size() > 0) |
| 368 | { |
| 369 | errorDoc = response.documents()[0]; |
| 370 | } |
| 371 | |
| 372 | return errorDoc; |
| 373 | } |
| 374 | |
| 375 | |
| 376 | std::string Database::getLastError(Connection& connection) const |
| 377 | { |
| 378 | Document::Ptr errorDoc = getLastErrorDoc(connection); |
| 379 | if (!errorDoc.isNull() && errorDoc->isType<std::string>("err" )) |
| 380 | { |
| 381 | return errorDoc->get<std::string>("err" ); |
| 382 | } |
| 383 | |
| 384 | return "" ; |
| 385 | } |
| 386 | |
| 387 | |
| 388 | Poco::SharedPtr<Poco::MongoDB::QueryRequest> Database::createCountRequest(const std::string& collectionName) const |
| 389 | { |
| 390 | Poco::SharedPtr<Poco::MongoDB::QueryRequest> request = createQueryRequest("$cmd" ); |
| 391 | request->setNumberToReturn(1); |
| 392 | request->selector().add("count" , collectionName); |
| 393 | return request; |
| 394 | } |
| 395 | |
| 396 | |
| 397 | } } // namespace Poco::MongoDB |
| 398 | |