Skip to content

PHPLIB-572: Add debugging tools #996

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

Merged
merged 5 commits into from
Oct 25, 2022
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
46 changes: 45 additions & 1 deletion docs/faq.txt
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,25 @@ SAPI. Additionally, :php:`php_ini_loaded_file() <php_ini_loaded_file>` and
:php:`php_ini_scanned_files() <php_ini_scanned_files>` may be used to determine
exactly which INI files have been loaded by PHP.

To debug issues with the extension not being loaded, you can use the
``detect-extension`` script provided in the tools directory. You can run this
script from the CLI or include it in a script accessible via your web server.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we expect people might run these scripts from a web SAPI, would it make sense to conditionally wrap the output in <pre>? Or I suppose people can just do that themselves -- there's really no reason to make these more complicated than they already are.

The tool will point out potential issues and installation instructions for your
system. Assuming you've installed the library through Composer, you can call the
script from the vendor directory:

.. code-block:: none

php vendor/mongodb/mongodb/tools/detect-extension.php

If you want to check configuration for a web server SAPI, include the file in
a script accessible from the web server and open it in your browser. Remember to
wrap the script in ``<pre>`` tags to properly format its output:

.. code-block:: php

<pre><?php require(...); ?></pre>

Loading an Incompatible DLL on Windows
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -80,7 +99,7 @@ for the correct environment.
Server Selection Failures
-------------------------

The follow are all examples of
The following are all examples of
:doc:`Server Selection </tutorial/server-selection>` failures:

.. code-block:: none
Expand Down Expand Up @@ -133,3 +152,28 @@ failure:
but was dropped or otherwise timeout out due to latency.
- "TLS handshake failed" suggests something related to TLS or OCSP verification
and is sometimes indicative of misconfigured TLS certificates.

In the case of a connection failure, you can use the ``connect`` tool to try and
receive more information. This tool attempts to connect to each host in a
connection string using socket functions to see if it is able to establish a
connection, send, and receive data. The tool takes the connection string to a
MongoDB deployment as its only argument. Assuming you've installed the library
through Composer, you would call the script from the vendor directory:

.. code-block:: none

php vendor/mongodb/mongodb/tools/connect.php mongodb://127.0.0.1:27017

In case the server does not accept connections, the output will look like this:

.. code-block:: none

Looking up MongoDB at mongodb://127.0.0.1:27017
Found 1 host(s) in the URI. Will attempt to connect to each.

Could not connect to 127.0.0.1:27017: Connection refused

.. note::

The tool only supports the ``mongodb://`` URI schema. Using the
``mongodb+srv`` scheme is not supported.
1 change: 1 addition & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<file>src</file>
<file>examples</file>
<file>tests</file>
<file>tools</file>

<!-- ****************************************** -->
<!-- Import rules from doctrine/coding-standard -->
Expand Down
137 changes: 137 additions & 0 deletions tools/connect.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

function getHosts(string $uri): array
{
if (strpos($uri, '://') === false) {
return [$uri];
}

$parsed = parse_url($uri);

if (isset($parsed['scheme']) && $parsed['scheme'] !== 'mongodb') {
// TODO: Resolve SRV records (https://github.com/mongodb/specifications/blob/master/source/initial-dns-seedlist-discovery/initial-dns-seedlist-discovery.rst)
throw new RuntimeException('Unsupported scheme: ' . $parsed['scheme']);
}

$hosts = sprintf('%s:%d', $parsed['host'], $parsed['port'] ?? 27017);

return explode(',', $hosts);
}

/** @param resource $stream */
function streamWrite($stream, string $data): int
{
for ($written = 0; $written < strlen($data); $written += $fwrite) {
$fwrite = fwrite($stream, substr($data, $written));

if ($fwrite === false) {
return $written;
}
}

return $written;
}

/** @param resource $stream */
function streamRead($stream, int $length): string
{
$contents = '';

while (! feof($stream) && strlen($contents) < $length) {
$fread = fread($stream, min($length - strlen($contents), 8192));

if ($fread === false) {
return $contents;
}

$contents .= $fread;
}

return $contents;
}

function connect(string $host, bool $ssl): void
{
$uri = sprintf('%s://%s', $ssl ? 'ssl' : 'tcp', $host);
$context = stream_context_create($ssl ? ['ssl' => ['capture_peer_cert' => true]] : []);
$client = @stream_socket_client($uri, $errno, $errorMessage, 5, STREAM_CLIENT_CONNECT, $context);

if ($client === false) {
printf("Could not connect to %s: %s\n", $host, $errorMessage);

return;
}

if ($ssl) {
$peerCertificate = stream_context_get_params($client)['options']['ssl']['peer_certificate'] ?? null;

if (! isset($peerCertificate)) {
printf("Could not capture peer certificate for %s\n", $host);

return;
}

$certificateProperties = openssl_x509_parse($peerCertificate);

// TODO: Check that the certificate common name (CN) matches the hostname
$now = new DateTime();
$validFrom = DateTime::createFromFormat('U', $certificateProperties['validFrom_time_t']);
$validTo = DateTime::createFromFormat('U', $certificateProperties['validTo_time_t']);
$isValid = $now >= $validFrom && $now <= $validTo;

printf("Peer certificate for %s is %s\n", $host, $isValid ? 'valid' : 'expired');

if (! $isValid) {
printf(" Valid from %s to %s\n", $validFrom->format('c'), $validTo->format('c'));
}
}

$request = pack(
'Va*xVVa*',
1 << 2 /* slaveOk */,
'admin.$cmd', /* namespace */
0, /* numberToSkip */
1, /* numberToReturn */
hex2bin('130000001069734d6173746572000100000000') /* { "isMaster": 1 } */
);
$requestLength = 16 /* MsgHeader length */ + strlen($request);
$header = pack('V4', $requestLength, 0 /* requestID */, 0 /* responseTo */, 2004 /* OP_QUERY */);

if ($requestLength !== streamWrite($client, $header . $request)) {
printf("Could not write request to %s\n", $host);

return;
}

$data = streamRead($client, 4);

if ($data === false || strlen($data) !== 4) {
printf("Could not read response header from %s\n", $host);

return;
}

[, $responseLength] = unpack('V', $data);

$data = streamRead($client, $responseLength - 4);

if ($data === false || strlen($data) !== $responseLength - 4) {
printf("Could not read response from %s\n", $host);

return;
}

printf("Successfully received response from %s\n", $host);
}

$uri = $argv[1] ?? 'mongodb://127.0.0.1';
printf("Looking up MongoDB at %s\n", $uri);
$hosts = getHosts($uri);
$ssl = stripos(parse_url($uri, PHP_URL_QUERY) ?? '', 'ssl=true') !== false;

printf("Found %d host(s) in the URI. Will attempt to connect to each.\n", count($hosts));

foreach ($hosts as $host) {
echo "\n";
connect($host, $ssl);
}
98 changes: 98 additions & 0 deletions tools/detect-extension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

function grepIniFile(string $filename, string $extension): int
{
$lines = [];

foreach (new SplFileObject($filename) as $i => $line) {
if (strpos($line, 'extension') === false) {
continue;
}

if (strpos($line, $extension) === false) {
continue;
}

$lines[$i] = $line;
}

if (empty($lines)) {
printf("No interesting lines in %s.\n", $filename);

return 0;
}

printf("Interesting lines in %s...\n", $filename);
foreach ($lines as $i => $line) {
printf(" %d: %s\n", $i + 1, trim($line));
}

return count($lines);
}

$extension = $argv[1] ?? 'mongodb';
$extensionDir = ini_get('extension_dir');

$version = phpversion($extension);

if ($version !== false) {
printf("Extension \"%s\" is loaded. Version: %s\n", $extension, $version);
exit;
}

printf("Extension \"%s\" is not loaded. Will attempt to scan INI files.\n", $extension);

// Check main INI file
$ini = php_ini_loaded_file();
$lines = 0;

if ($ini === false) {
printf("No php.ini file is loaded. Will attempt to scan additional INI files.\n");
} else {
$lines += grepIniFile($ini, $extension);
}

// Check additional INI files in scan directory
// See: https://www.php.net/manual/en/configuration.file.php#configuration.file.scan
$files = php_ini_scanned_files();

if (empty($files)) {
printf("No additional INI files are loaded. Nothing left to scan.\n");
} else {
foreach (explode(',', $files) as $ini) {
$lines += grepIniFile(trim($ini), $extension);
}
}

$mask = defined('PHP_WINDOWS_VERSION_BUILD') ? 'php_%s.dll' : '%s.so';
$filename = sprintf($mask, $extension);
$extensionFileExists = file_exists($extensionDir . '/' . $filename);

echo "\n";
printf("PHP will look for extensions in: %s\n", $extensionDir);
printf("Checking if that directory is readable: %s\n", is_dir($extensionDir) || ! is_readable($extensionDir) ? 'yes' : 'no');
printf("Checking if extension file exists in that directory: %s\n", $extensionFileExists ? 'yes' : 'no');
echo "\n";

if ($extensionFileExists) {
printf("A file named %s exists in the extension directory. Make sure you have enabled the extension in php.ini.\n", $filename);
} elseif (! defined('PHP_WINDOWS_VERSION_BUILD')) {
// Installation instructions for non-Windows systems are only necessary if the extension file does not exist.
printf("You should install the extension using the pecl command in %s\n", PHP_BINDIR);
printf("After installing the extension, you should add \"extension=%s\" to php.ini\n", $filename);
}

if (defined('PHP_WINDOWS_VERSION_BUILD')) {
$zts = PHP_ZTS ? 'Thread Safe (TS)' : 'Non Thread Safe (NTS)';
$arch = PHP_INT_SIZE === 8 ? 'x64' : 'x86';
$dll = sprintf("%d.%d %s %s", PHP_MAJOR_VERSION, PHP_MINOR_VERSION, $zts, $arch);

printf("You likely need to download a Windows DLL for: %s\n", $dll);
printf("Windows DLLs should be available from: https://pecl.php.net/package/%s\n", $extension);

if ($extensionFileExists) {
echo "If you have enabled the extension in php.ini and it is not loading, make sure you have downloaded the correct DLL file as indicated above.\n";
} else {
printf("After installing the extension, you should add \"extension=%s\" to php.ini\n", $filename);
}
}