Skip to content

Commit

Permalink
PHPLIB-572: Add debugging tools (#996)
Browse files Browse the repository at this point in the history
* Add connection debugging script

* Fix grammar

* Add extension detection script

* Fix wording feedback

* Document using pre tags for formatted output in web SAPI
  • Loading branch information
alcaeus committed Oct 25, 2022
1 parent 7b3eca3 commit 56e8a05
Show file tree
Hide file tree
Showing 4 changed files with 281 additions and 1 deletion.
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.
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);
}
}

0 comments on commit 56e8a05

Please sign in to comment.