Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do mongo handshake and store ServerDescription on connection #2201

Merged
merged 12 commits into from Dec 11, 2019
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -37,7 +37,7 @@ d:
- dmd-beta

env:
- VIBED_DRIVER=vibe-core PARTS=builds,unittests,examples,tests
- VIBED_DRIVER=vibe-core PARTS=builds,unittests,examples,tests,mongo

matrix:
include:
Expand Down
147 changes: 139 additions & 8 deletions mongodb/vibe/db/mongo/connection.d
Expand Up @@ -9,6 +9,7 @@ module vibe.db.mongo.connection;

public import vibe.data.bson;

import vibe.core.core : vibeVersionString;
import vibe.core.log;
import vibe.core.net;
import vibe.db.mongo.settings;
Expand All @@ -18,11 +19,12 @@ import vibe.stream.tls;

import std.algorithm : map, splitter;
import std.array;
import std.range;
import std.conv;
import std.digest.md;
import std.exception;
import std.range;
import std.string;
import std.digest.md;
import std.typecons;


private struct _MongoErrorDescription
Expand Down Expand Up @@ -127,6 +129,10 @@ final class MongoConnection {
ulong m_bytesRead;
int m_msgid = 1;
StreamOutputRange!(InterfaceProxy!Stream) m_outRange;
ServerDescription m_description;
/// Flag to prevent recursive connections when server closes connection while connecting
bool m_allowReconnect;
bool m_isAuthenticating;
}

enum ushort defaultPort = MongoClientSettings.defaultPort;
Expand Down Expand Up @@ -181,10 +187,48 @@ final class MongoConnection {
throw new MongoDriverException(format("Failed to connect to MongoDB server at %s:%s.", m_settings.hosts[0].name, m_settings.hosts[0].port), __FILE__, __LINE__, e);
}

m_allowReconnect = false;
scope (exit)
m_allowReconnect = true;
WebFreak001 marked this conversation as resolved.
Show resolved Hide resolved

Bson handshake = Bson.emptyObject;
handshake["isMaster"] = Bson(1);

import os = std.system;
import compiler = std.compiler;
string platform = compiler.name ~ " "
~ compiler.version_major.to!string ~ "." ~ compiler.version_minor.to!string;
// TODO: add support for os.version

handshake["client"] = Bson([
"driver": Bson(["name": Bson("vibe.db.mongo"), "version": Bson(vibeVersionString)]),
"os": Bson(["type": Bson(os.os.to!string), "architecture": Bson(hostArchitecture)]),
"platform": Bson(platform)
]);

if (m_settings.appName.length) {
enforce!MongoAuthException(m_settings.appName.length <= 128,
"The application name may not be larger than 128 bytes");
handshake["client"]["application"] = Bson(["name": Bson(m_settings.appName)]);
}

query!Bson("$external.$cmd", QueryFlags.none, 0, -1, handshake, Bson(null),
(cursor, flags, first_doc, num_docs) {
enforce!MongoDriverException(!(flags & ReplyFlags.QueryFailure) && num_docs == 1,
"Authentication handshake failed.");
},
(idx, ref doc) {
enforce!MongoAuthException(doc["ok"].get!double == 1.0, "Authentication failed.");
m_description = deserializeBson!ServerDescription(doc);
});

m_bytesRead = 0;
if(m_settings.digest != string.init)
{
if (m_settings.authMechanism == MongoAuthMechanism.mongoDBCR)
m_isAuthenticating = true;
scope (exit)
m_isAuthenticating = false;
if (m_settings.authMechanism == MongoAuthMechanism.mongoDBCR || !m_description.satisfiesVersion(WireVersion.v30))
authenticate(); // use old mechanism if explicitly stated
else {
/**
Expand Down Expand Up @@ -225,6 +269,7 @@ final class MongoConnection {

@property bool connected() const { return m_conn && m_conn.connected; }

@property const(ServerDescription) description() const { return m_description; }

void update(string collection_name, UpdateFlags flags, Bson selector, Bson update)
{
Expand Down Expand Up @@ -403,6 +448,8 @@ final class MongoConnection {
auto bson = () @trusted { return recvBson(buf); } ();
}

// logDebugV("Received mongo response on %s:%s: %s", reqid, i, bson);

static if (is(T == Bson)) on_doc(i, bson);
else {
T doc = deserializeBson!T(bson);
Expand All @@ -415,14 +462,20 @@ final class MongoConnection {

private int send(ARGS...)(OpCode code, int response_to, ARGS args)
{
if( !connected() ) connect();
if( !connected() ) {
if (m_allowReconnect) connect();
else if (m_isAuthenticating) throw new MongoAuthException("Connection got closed while authenticating");
else throw new MongoDriverException("Connection got closed while connecting");
}
int id = nextMessageId();
sendValue(16 + sendLength(args));
sendValue(id);
sendValue(response_to);
sendValue(cast(int)code);
// sendValue!int to make sure we don't accidentally send other types after arithmetic operations/changing types
WebFreak001 marked this conversation as resolved.
Show resolved Hide resolved
sendValue!int(16 + sendLength(args));
sendValue!int(id);
sendValue!int(response_to);
sendValue!int(cast(int)code);
foreach (a; args) sendValue(a);
m_outRange.flush();
// logDebugV("Sent mongo opcode %s (id %s) in response to %s with args %s", code, id, response_to, tuple(args));
return id;
}

Expand Down Expand Up @@ -627,3 +680,81 @@ private int sendLength(ARGS...)(ARGS args)
else if (ARGS.length == 0) return 0;
else return sendLength(args[0 .. $/2]) + sendLength(args[$/2 .. $]);
}

struct ServerDescription
{
enum ServerType
{
unknown,
standalone,
mongos,
possiblePrimary,
RSPrimary,
RSSecondary,
RSArbiter,
RSOther,
RSGhost
}

@optional:
string address;
string error;
float roundTripTime = 0;
Nullable!BsonDate lastWriteDate;
Nullable!BsonObjectID opTime;
ServerType type = ServerType.unknown;
WireVersion minWireVersion, maxWireVersion;
string me;
string[] hosts, passives, arbiters;
string[string] tags;
string setName;
Nullable!int setVersion;
Nullable!BsonObjectID electionId;
string primary;
string lastUpdateTime = "infinity ago";
Nullable!int logicalSessionTimeoutMinutes;

bool satisfiesVersion(WireVersion wireVersion) @safe const @nogc pure nothrow
{
return maxWireVersion >= wireVersion;
}
}

enum WireVersion : int
{
old,
v26,
v26_2,
v30,
v32,
v34,
v36,
v40,
v42
}

private string getHostArchitecture()
{
import os = std.system;

version (X86_64)
string arch = "x86_64 ";
else version (X86)
string arch = "x86 ";
else version (AArch64)
string arch = "aarch64 ";
else version (ARM_HardFloat)
string arch = "armhf ";
else version (ARM)
string arch = "arm ";
else version (PPC64)
string arch = "ppc64 ";
else version (PPC)
string arch = "ppc ";
else
string arch = "unknown ";

return arch ~ os.endian.to!string;
}

private static immutable hostArchitecture = getHostArchitecture;
34 changes: 33 additions & 1 deletion mongodb/vibe/db/mongo/sasl.d
Expand Up @@ -15,6 +15,7 @@ import std.digest.sha;
import std.exception;
import std.format;
import std.string;
import std.traits;
import std.utf;
import vibe.crypto.cryptorand;

Expand Down Expand Up @@ -46,7 +47,16 @@ package struct ScramState
return format("n,,%s", m_firstMessageBare);
}

string update(string password, string challenge)
version (unittest) private string createInitialRequestWithFixedNonce(string user, string nonce)
{
m_nonce = nonce;

m_firstMessageBare = format("n=%s,r=%s", escapeUsername(user), m_nonce);
return format("n,,%s", m_firstMessageBare);
}

// MongoDB drivers require 4096 min iterations https://github.com/mongodb/specifications/blob/59390a7ab2d5c8f9c29b8af1775ff25915c44036/source/auth/auth.rst#scram-sha-1
string update(string password, string challenge, int minIterations = 4096)
{
string serverFirstMessage = challenge;

Expand All @@ -62,6 +72,9 @@ package struct ScramState
throw new Exception("Invalid server challenge format");
int iterations = next[3 .. $].to!int();

if (iterations < minIterations)
throw new Exception("Server must request at least " ~ minIterations.to!string ~ " iterations");

if (serverNonce[0 .. m_nonce.length] != m_nonce)
throw new Exception("Invalid server nonce received");
string finalMessage = format("c=biws,r=%s", serverNonce);
Expand Down Expand Up @@ -144,6 +157,8 @@ private DigestType!SHA1 pbkdf2(const ubyte[] password, const ubyte[] salt, int i

ubyte[4] intBytes = [0, 0, 0, 1];
auto last = () @trusted { return hmac!SHA1(salt, intBytes[], password); } ();
static assert(isStaticArray!(typeof(last)),
"Code is written so that the hash array is expected to be placed on the stack");
auto current = last;
foreach (i; 1 .. iterations)
{
Expand All @@ -155,3 +170,20 @@ private DigestType!SHA1 pbkdf2(const ubyte[] password, const ubyte[] salt, int i
}
return current;
}

unittest
{
// https://github.com/mongodb/specifications/blob/59390a7ab2d5c8f9c29b8af1775ff25915c44036/source/auth/auth.rst#id5

import vibe.db.mongo.settings : MongoClientSettings;

ScramState state;
assert(state.createInitialRequestWithFixedNonce("user", "fyko+d2lbbFgONRv9qkxdawL")
== "n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL");
auto last = state.update(MongoClientSettings.makeDigest("user", "pencil"),
"r=fyko+d2lbbFgONRv9qkxdawLHo+Vgk7qvUOKUwuWLIWg4l/9SraGMHEE,s=rQ9ZY3MntBeuP3E1TDVC4w==,i=10000");
assert(last == "c=biws,r=fyko+d2lbbFgONRv9qkxdawLHo+Vgk7qvUOKUwuWLIWg4l/9SraGMHEE,p=MC2T8BvbmWRckDw8oWl5IVghwCY=",
last);
last = state.finalize("v=UMWeI25JD1yNYZRMpZ4VHvhZ9e0=");
assert(last == "", last);
}
3 changes: 3 additions & 0 deletions mongodb/vibe/db/mongo/settings.d
Expand Up @@ -157,6 +157,7 @@ bool parseMongoDBUrl(out MongoClientSettings cfg, string url)

switch( option.toLower() ){
default: logWarn("Unknown MongoDB option %s", option); break;
case "appname": cfg.appName = value; break;
case "slaveok": bool v; if( setBool(v) && v ) cfg.defQueryFlags |= QueryFlags.SlaveOk; break;
case "replicaset": cfg.replicaSet = value; warnNotImplemented(); break;
case "safe": setBool(cfg.safe); break;
Expand Down Expand Up @@ -341,6 +342,8 @@ class MongoClientSettings
string sslPEMKeyFile;
string sslCAFile;
MongoAuthMechanism authMechanism;
/// Application name for the connection information when connected.
string appName;

static string makeDigest(string username, string password)
@safe {
Expand Down
3 changes: 3 additions & 0 deletions tests/mongodb/.gitignore
@@ -0,0 +1,3 @@
db/
log.txt*
dump/
7 changes: 7 additions & 0 deletions tests/mongodb/_connection/dub.json
@@ -0,0 +1,7 @@
{
"name": "tests",
"description": "MongoDB connection tests",
"dependencies": {
"vibe-d:mongodb": {"path": "../../../"}
}
}
64 changes: 64 additions & 0 deletions tests/mongodb/_connection/run.sh
@@ -0,0 +1,64 @@
#!/bin/bash

# This test file uses run.sh to manually start the mongod server with different authentication schemes and run dub multiple times with different expected test results.

MONGOPORT=22824
rm -f log.txt*
rm -rf db
mkdir -p db/noauth
mkdir -p db/wcert
mkdir -p db/auth

if ! eval $DUB_INVOKE -- $MONGOPORT failconnect ; then
exit 1
fi

# We use --fork in all mongod calls because it waits until the database is fully up-and-running for all queries.

# Unauthenticated Test

MONGOPID=$(mongod --logpath log.txt --bind_ip 127.0.0.1 --port $MONGOPORT --noauth --dbpath db/noauth --fork | grep -Po 'forked process: \K\d+')
WebFreak001 marked this conversation as resolved.
Show resolved Hide resolved

if ! eval $DUB_INVOKE -- $MONGOPORT ; then
kill $MONGOPID
exit 1
else
kill $MONGOPID
fi
((MONGOPORT++))

# TODO: Certificate Auth Test

# Authenticated Test

MONGOPID=$(mongod --logpath log.txt --bind_ip 127.0.0.1 --port $MONGOPORT --noauth --dbpath db/auth --fork | grep -Po 'forked process: \K\d+')
echo "db.createUser({user:'admin',pwd:'123456',roles:[{role:'readWrite',db:'unittest'},'dbAdmin'],passwordDigestor:'server'})" | mongo "mongodb://127.0.0.1:$MONGOPORT/admin"
kill $MONGOPID

while kill -0 $MONGOPID; do
sleep 1
done

MONGOPID=$(mongod --logpath log.txt --bind_ip 127.0.0.1 --port $MONGOPORT --auth --dbpath db/auth --fork | grep -Po 'forked process: \K\d+')
sleep 1

echo Trying unauthenticated operations on a protected database
if ! eval $DUB_INVOKE -- $MONGOPORT faildb ; then
kill $MONGOPID
exit 1
fi

echo Trying wrongly authenticated operations on a protected database
if ! eval $DUB_INVOKE -- $MONGOPORT failauth auth admin 1234567 ; then
kill $MONGOPID
exit 1
fi

echo Trying authenticated operations on a protected database
if ! eval $DUB_INVOKE -- $MONGOPORT auth admin 123456 ; then
kill $MONGOPID
exit 1
fi

kill $MONGOPID
((MONGOPORT++))