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

Support connecting to Azure Cosmos DB #53

Merged
merged 8 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ MongoDB for XP Framework ChangeLog

## ?.?.? / ????-??-??

* Merged PR #53: Support connecting to Azure Cosmos DB:
- Fixed *Expected type binData but found string.* during authentication
- Try both `hello` and `isMaster` commands to retrieve server information
- Handle hostname aliases returned inside server information
See https://www.mongodb.com/docs/drivers/cosmosdb-support/
(@thekid)

## 3.0.0 / 2025-01-25

* **Heads up:** Dropped support for PHP < 7.4 as well as XP Core <= 9.
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/com/mongodb/auth/Scram.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public function conversation(string $username, string $password, string $authsou
// Step 3: After having verified server signature, finalize
yield [
'saslContinue' => 1,
'payload' => '',
'payload' => new Bytes([]),
'conversationId' => $next['conversationId'],
'$db' => $authsource,
];
Expand Down
62 changes: 35 additions & 27 deletions src/main/php/com/mongodb/io/Connection.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use com\mongodb\{Authentication, AuthenticationFailed, Error};
use lang\Throwable;
use peer\{Socket, ProtocolException, ConnectException};
use util\Secret;
use util\{Secret, Objects};

/**
* A single connection to MongoDB server, of which more than one may exist
Expand Down Expand Up @@ -98,7 +98,7 @@ public function establish($options= [], $auth= null) {
$params= [
'client' => [
'application' => ['name' => $options['params']['appName'] ?? $_SERVER['argv'][0] ?? 'php'],
'driver' => ['name' => 'XP MongoDB Connectivity', 'version' => '1.0.0'],
'driver' => ['name' => 'XP MongoDB Connectivity', 'version' => '3.0.0'],
'os' => ['name' => php_uname('s'), 'type' => PHP_OS, 'architecture' => php_uname('m'), 'version' => php_uname('r')]
]
];
Expand Down Expand Up @@ -151,34 +151,42 @@ public function establish($options= [], $auth= null) {
* @throws peer.ProtocolException
*/
public function hello($command= []) {
$reply= $this->send(
self::OP_QUERY,
"\x00\x00\x00\x00admin.\$cmd\x00\x00\x00\x00\x00\x01\x00\x00\x00",
['hello' => 1] + $command
);

// See https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-discovery-and-monitoring.rst#type
$document= &$reply['documents'][0];
$kind= self::Standalone;
if (isset($document['isreplicaset'])) {
$kind= self::RSGhost;
} else if ('' !== ($document['setName'] ?? '')) {
if ($document['isWritablePrimary'] ?? null) {
$kind= self::RSPrimary;
} else if ($document['hidden'] ?? null) {
$kind= self::RSMember;
} else if ($document['secondary'] ?? null) {
$kind= self::RSSecondary;
} else if ($document['arbiterOnly'] ?? null) {
$kind= self::RSArbiter;
} else {
$kind= self::RSMember;

// Try both `hello` and `isMaster` - the latter is for Azure CosmosDB compatibility, see
// https://feedback.azure.com/d365community/idea/c7b19748-9276-ef11-a4e6-000d3a059eeb
foreach (['hello', 'isMaster'] as $variant) {
$reply= $this->send(
self::OP_QUERY,
"\x00\x00\x00\x00admin.\$cmd\x00\x00\x00\x00\x00\x01\x00\x00\x00",
[$variant => 1] + $command
);
$document= &$reply['documents'][0];

// See https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-discovery-and-monitoring.rst#type
if ($document['ok'] ?? false) {
if (isset($document['isreplicaset'])) {
$kind= self::RSGhost;
} else if ('' !== ($document['setName'] ?? '')) {
if ($document['isWritablePrimary'] ?? null) {
$kind= self::RSPrimary;
} else if ($document['hidden'] ?? null) {
$kind= self::RSMember;
} else if ($document['secondary'] ?? null) {
$kind= self::RSSecondary;
} else if ($document['arbiterOnly'] ?? null) {
$kind= self::RSArbiter;
} else {
$kind= self::RSMember;
}
} else if ('isdbgrid' === ($document['msg'] ?? '')) {
$kind= self::Mongos;
}

return ['$kind' => $kind ?? self::Standalone] + $document;
}
} else if ('isdbgrid' === ($document['msg'] ?? '')) {
$kind= self::Mongos;
}

return ['$kind' => $kind] + $document;
throw new ProtocolException($variant.' command failed: '.Objects::stringOf($document));
}

/**
Expand Down
7 changes: 6 additions & 1 deletion src/main/php/com/mongodb/io/Protocol.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,12 @@ public function establish($candidates, $intent) {
// Refresh view into cluster every time we succesfully connect to a node. For sockets that
// have not been used for socketCheckInterval, issue the ping command to check liveness.
if (null === $conn->server) {
connect: $this->useCluster($conn->establish($this->options, $this->auth));
connect: $me= $conn->establish($this->options, $this->auth)['me'] ?? $candidate;
if ($candidate !== $me) {
unset($this->conn[$candidate]);
$this->conn[$me]= $conn;
}
$this->useCluster($conn->server);
} else if ($time - $conn->lastUsed >= $this->socketCheckInterval) {
try {
$conn->timeout(1)->send(Connection::OP_MSG, "\x00\x00\x00\x00\x00", ['ping' => 1, '$db' => 'admin']);
Expand Down
47 changes: 47 additions & 0 deletions src/test/php/com/mongodb/unittest/MongoConnectionTest.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,53 @@ public function connect() {
Assert::true($protocol->connections()[self::$PRIMARY]->connected());
}

#[Test, Values(['example.mongo.cosmos.azure.com:10255', 'example-germanywestcentral.mongo.cosmos.azure.com:10255'])]
public function connect_to_azure_cosmos_db($resolved) {
$protocol= $this->protocol(['example.mongo.cosmos.azure.com:10255' => [
[
'responseFlags' => 0,
'cursorID' => 0,
'startingFrom' => 0,
'numberReturned' => 1,
'documents' => [[
'ok' => 0,
'code' => 115,
'codeName' => 'CommandNotSupported',
'errmsg' => 'Command Hello not supported prior to authentication',
]],
],
[
'responseFlags' => 0,
'cursorID' => 0,
'startingFrom' => 0,
'numberReturned' => 1,
'documents' => [[
'ismaster' => true,
'maxBsonObjectSize' => 16777216,
'maxMessageSizeBytes' => 48000000,
'maxWriteBatchSize' => 1000,
'localTime' => new Int64(1738183466672),
'logicalSessionTimeoutMinutes' => 30,
'minWireVersion' => 0,
'maxWireVersion' => 18,
'readOnly' => false,
'tags' => ['region' => 'Germany West Central'],
'hosts' => [$resolved],
'setName' => 'globaldb',
'setVersion' => 1,
'primary' => $resolved,
'me' => $resolved,
'connectionId' => 382867193,
'ok' => 1.0
]]
],
]]);
$fixture= new MongoConnection($protocol);
$fixture->connect();

Assert::true($protocol->connections()[$resolved]->connected());
}

#[Test]
public function close() {
$protocol= $this->protocol([$this->hello(self::$PRIMARY)]);
Expand Down
2 changes: 1 addition & 1 deletion src/test/php/com/mongodb/unittest/ScramSHA1Test.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public function final_message() {
Assert::equals(
[
'saslContinue' => 1,
'payload' => '',
'payload' => new Bytes([]),
'conversationId' => 1,
'$db' => self::DATABASE
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public function final_message() {
Assert::equals(
[
'saslContinue' => 1,
'payload' => '',
'payload' => new Bytes([]),
'conversationId' => 1,
'$db' => self::DATABASE
],
Expand Down