Skip to content

Commit 56e8a05

Browse files
authored
PHPLIB-572: Add debugging tools (#996)
* Add connection debugging script * Fix grammar * Add extension detection script * Fix wording feedback * Document using pre tags for formatted output in web SAPI
1 parent 7b3eca3 commit 56e8a05

File tree

4 files changed

+281
-1
lines changed

4 files changed

+281
-1
lines changed

docs/faq.txt

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,25 @@ SAPI. Additionally, :php:`php_ini_loaded_file() <php_ini_loaded_file>` and
5353
:php:`php_ini_scanned_files() <php_ini_scanned_files>` may be used to determine
5454
exactly which INI files have been loaded by PHP.
5555

56+
To debug issues with the extension not being loaded, you can use the
57+
``detect-extension`` script provided in the tools directory. You can run this
58+
script from the CLI or include it in a script accessible via your web server.
59+
The tool will point out potential issues and installation instructions for your
60+
system. Assuming you've installed the library through Composer, you can call the
61+
script from the vendor directory:
62+
63+
.. code-block:: none
64+
65+
php vendor/mongodb/mongodb/tools/detect-extension.php
66+
67+
If you want to check configuration for a web server SAPI, include the file in
68+
a script accessible from the web server and open it in your browser. Remember to
69+
wrap the script in ``<pre>`` tags to properly format its output:
70+
71+
.. code-block:: php
72+
73+
<pre><?php require(...); ?></pre>
74+
5675
Loading an Incompatible DLL on Windows
5776
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5877

@@ -80,7 +99,7 @@ for the correct environment.
8099
Server Selection Failures
81100
-------------------------
82101

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

86105
.. code-block:: none
@@ -133,3 +152,28 @@ failure:
133152
but was dropped or otherwise timeout out due to latency.
134153
- "TLS handshake failed" suggests something related to TLS or OCSP verification
135154
and is sometimes indicative of misconfigured TLS certificates.
155+
156+
In the case of a connection failure, you can use the ``connect`` tool to try and
157+
receive more information. This tool attempts to connect to each host in a
158+
connection string using socket functions to see if it is able to establish a
159+
connection, send, and receive data. The tool takes the connection string to a
160+
MongoDB deployment as its only argument. Assuming you've installed the library
161+
through Composer, you would call the script from the vendor directory:
162+
163+
.. code-block:: none
164+
165+
php vendor/mongodb/mongodb/tools/connect.php mongodb://127.0.0.1:27017
166+
167+
In case the server does not accept connections, the output will look like this:
168+
169+
.. code-block:: none
170+
171+
Looking up MongoDB at mongodb://127.0.0.1:27017
172+
Found 1 host(s) in the URI. Will attempt to connect to each.
173+
174+
Could not connect to 127.0.0.1:27017: Connection refused
175+
176+
.. note::
177+
178+
The tool only supports the ``mongodb://`` URI schema. Using the
179+
``mongodb+srv`` scheme is not supported.

phpcs.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<file>src</file>
1313
<file>examples</file>
1414
<file>tests</file>
15+
<file>tools</file>
1516

1617
<!-- ****************************************** -->
1718
<!-- Import rules from doctrine/coding-standard -->

tools/connect.php

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
function getHosts(string $uri): array
4+
{
5+
if (strpos($uri, '://') === false) {
6+
return [$uri];
7+
}
8+
9+
$parsed = parse_url($uri);
10+
11+
if (isset($parsed['scheme']) && $parsed['scheme'] !== 'mongodb') {
12+
// TODO: Resolve SRV records (https://github.com/mongodb/specifications/blob/master/source/initial-dns-seedlist-discovery/initial-dns-seedlist-discovery.rst)
13+
throw new RuntimeException('Unsupported scheme: ' . $parsed['scheme']);
14+
}
15+
16+
$hosts = sprintf('%s:%d', $parsed['host'], $parsed['port'] ?? 27017);
17+
18+
return explode(',', $hosts);
19+
}
20+
21+
/** @param resource $stream */
22+
function streamWrite($stream, string $data): int
23+
{
24+
for ($written = 0; $written < strlen($data); $written += $fwrite) {
25+
$fwrite = fwrite($stream, substr($data, $written));
26+
27+
if ($fwrite === false) {
28+
return $written;
29+
}
30+
}
31+
32+
return $written;
33+
}
34+
35+
/** @param resource $stream */
36+
function streamRead($stream, int $length): string
37+
{
38+
$contents = '';
39+
40+
while (! feof($stream) && strlen($contents) < $length) {
41+
$fread = fread($stream, min($length - strlen($contents), 8192));
42+
43+
if ($fread === false) {
44+
return $contents;
45+
}
46+
47+
$contents .= $fread;
48+
}
49+
50+
return $contents;
51+
}
52+
53+
function connect(string $host, bool $ssl): void
54+
{
55+
$uri = sprintf('%s://%s', $ssl ? 'ssl' : 'tcp', $host);
56+
$context = stream_context_create($ssl ? ['ssl' => ['capture_peer_cert' => true]] : []);
57+
$client = @stream_socket_client($uri, $errno, $errorMessage, 5, STREAM_CLIENT_CONNECT, $context);
58+
59+
if ($client === false) {
60+
printf("Could not connect to %s: %s\n", $host, $errorMessage);
61+
62+
return;
63+
}
64+
65+
if ($ssl) {
66+
$peerCertificate = stream_context_get_params($client)['options']['ssl']['peer_certificate'] ?? null;
67+
68+
if (! isset($peerCertificate)) {
69+
printf("Could not capture peer certificate for %s\n", $host);
70+
71+
return;
72+
}
73+
74+
$certificateProperties = openssl_x509_parse($peerCertificate);
75+
76+
// TODO: Check that the certificate common name (CN) matches the hostname
77+
$now = new DateTime();
78+
$validFrom = DateTime::createFromFormat('U', $certificateProperties['validFrom_time_t']);
79+
$validTo = DateTime::createFromFormat('U', $certificateProperties['validTo_time_t']);
80+
$isValid = $now >= $validFrom && $now <= $validTo;
81+
82+
printf("Peer certificate for %s is %s\n", $host, $isValid ? 'valid' : 'expired');
83+
84+
if (! $isValid) {
85+
printf(" Valid from %s to %s\n", $validFrom->format('c'), $validTo->format('c'));
86+
}
87+
}
88+
89+
$request = pack(
90+
'Va*xVVa*',
91+
1 << 2 /* slaveOk */,
92+
'admin.$cmd', /* namespace */
93+
0, /* numberToSkip */
94+
1, /* numberToReturn */
95+
hex2bin('130000001069734d6173746572000100000000') /* { "isMaster": 1 } */
96+
);
97+
$requestLength = 16 /* MsgHeader length */ + strlen($request);
98+
$header = pack('V4', $requestLength, 0 /* requestID */, 0 /* responseTo */, 2004 /* OP_QUERY */);
99+
100+
if ($requestLength !== streamWrite($client, $header . $request)) {
101+
printf("Could not write request to %s\n", $host);
102+
103+
return;
104+
}
105+
106+
$data = streamRead($client, 4);
107+
108+
if ($data === false || strlen($data) !== 4) {
109+
printf("Could not read response header from %s\n", $host);
110+
111+
return;
112+
}
113+
114+
[, $responseLength] = unpack('V', $data);
115+
116+
$data = streamRead($client, $responseLength - 4);
117+
118+
if ($data === false || strlen($data) !== $responseLength - 4) {
119+
printf("Could not read response from %s\n", $host);
120+
121+
return;
122+
}
123+
124+
printf("Successfully received response from %s\n", $host);
125+
}
126+
127+
$uri = $argv[1] ?? 'mongodb://127.0.0.1';
128+
printf("Looking up MongoDB at %s\n", $uri);
129+
$hosts = getHosts($uri);
130+
$ssl = stripos(parse_url($uri, PHP_URL_QUERY) ?? '', 'ssl=true') !== false;
131+
132+
printf("Found %d host(s) in the URI. Will attempt to connect to each.\n", count($hosts));
133+
134+
foreach ($hosts as $host) {
135+
echo "\n";
136+
connect($host, $ssl);
137+
}

tools/detect-extension.php

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
3+
function grepIniFile(string $filename, string $extension): int
4+
{
5+
$lines = [];
6+
7+
foreach (new SplFileObject($filename) as $i => $line) {
8+
if (strpos($line, 'extension') === false) {
9+
continue;
10+
}
11+
12+
if (strpos($line, $extension) === false) {
13+
continue;
14+
}
15+
16+
$lines[$i] = $line;
17+
}
18+
19+
if (empty($lines)) {
20+
printf("No interesting lines in %s.\n", $filename);
21+
22+
return 0;
23+
}
24+
25+
printf("Interesting lines in %s...\n", $filename);
26+
foreach ($lines as $i => $line) {
27+
printf(" %d: %s\n", $i + 1, trim($line));
28+
}
29+
30+
return count($lines);
31+
}
32+
33+
$extension = $argv[1] ?? 'mongodb';
34+
$extensionDir = ini_get('extension_dir');
35+
36+
$version = phpversion($extension);
37+
38+
if ($version !== false) {
39+
printf("Extension \"%s\" is loaded. Version: %s\n", $extension, $version);
40+
exit;
41+
}
42+
43+
printf("Extension \"%s\" is not loaded. Will attempt to scan INI files.\n", $extension);
44+
45+
// Check main INI file
46+
$ini = php_ini_loaded_file();
47+
$lines = 0;
48+
49+
if ($ini === false) {
50+
printf("No php.ini file is loaded. Will attempt to scan additional INI files.\n");
51+
} else {
52+
$lines += grepIniFile($ini, $extension);
53+
}
54+
55+
// Check additional INI files in scan directory
56+
// See: https://www.php.net/manual/en/configuration.file.php#configuration.file.scan
57+
$files = php_ini_scanned_files();
58+
59+
if (empty($files)) {
60+
printf("No additional INI files are loaded. Nothing left to scan.\n");
61+
} else {
62+
foreach (explode(',', $files) as $ini) {
63+
$lines += grepIniFile(trim($ini), $extension);
64+
}
65+
}
66+
67+
$mask = defined('PHP_WINDOWS_VERSION_BUILD') ? 'php_%s.dll' : '%s.so';
68+
$filename = sprintf($mask, $extension);
69+
$extensionFileExists = file_exists($extensionDir . '/' . $filename);
70+
71+
echo "\n";
72+
printf("PHP will look for extensions in: %s\n", $extensionDir);
73+
printf("Checking if that directory is readable: %s\n", is_dir($extensionDir) || ! is_readable($extensionDir) ? 'yes' : 'no');
74+
printf("Checking if extension file exists in that directory: %s\n", $extensionFileExists ? 'yes' : 'no');
75+
echo "\n";
76+
77+
if ($extensionFileExists) {
78+
printf("A file named %s exists in the extension directory. Make sure you have enabled the extension in php.ini.\n", $filename);
79+
} elseif (! defined('PHP_WINDOWS_VERSION_BUILD')) {
80+
// Installation instructions for non-Windows systems are only necessary if the extension file does not exist.
81+
printf("You should install the extension using the pecl command in %s\n", PHP_BINDIR);
82+
printf("After installing the extension, you should add \"extension=%s\" to php.ini\n", $filename);
83+
}
84+
85+
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
86+
$zts = PHP_ZTS ? 'Thread Safe (TS)' : 'Non Thread Safe (NTS)';
87+
$arch = PHP_INT_SIZE === 8 ? 'x64' : 'x86';
88+
$dll = sprintf("%d.%d %s %s", PHP_MAJOR_VERSION, PHP_MINOR_VERSION, $zts, $arch);
89+
90+
printf("You likely need to download a Windows DLL for: %s\n", $dll);
91+
printf("Windows DLLs should be available from: https://pecl.php.net/package/%s\n", $extension);
92+
93+
if ($extensionFileExists) {
94+
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";
95+
} else {
96+
printf("After installing the extension, you should add \"extension=%s\" to php.ini\n", $filename);
97+
}
98+
}

0 commit comments

Comments
 (0)