1818use React \Socket \ConnectionInterface ;
1919use React \Socket \Connector ;
2020use RuntimeException ;
21+ use function explode ;
22+ use function inet_pton ;
23+ use function openssl_x509_fingerprint ;
24+ use function openssl_x509_parse ;
2125use function parse_url ;
26+ use function str_replace ;
27+ use function stream_context_get_params ;
2228use function strlen ;
2329use function strtolower ;
2430use function substr_count ;
31+ use function trim ;
2532
2633class DohExecutor implements ExecutorInterface {
2734
@@ -41,6 +48,9 @@ class DohExecutor implements ExecutorInterface {
4148
4249 const FINGERPRINT_HASH_METHOD = 'sha256 ' ;
4350
51+ /** @var bool Flag for identifying when we need to workaround php-src issue GH-9356 */
52+ const NEED_GH9356_IPV6_WORKAROUND = (\PHP_VERSION_ID < 80121 || (\PHP_VERSION_ID >= 80200 && \PHP_VERSION_ID < 80208 ));
53+
4454 /**
4555 * @param string $nameserver
4656 * @param string $method
@@ -137,60 +147,8 @@ private function getBrowser() : Promise\PromiseInterface {
137147 if (!isset ($ this ->browserResolution )) {
138148 $ deferred = new Deferred ();
139149 $ this ->browserResolution = $ deferred ->promise ();
140- if ($ this ->ipv6address ) {
141- // PHP does not validate IPv6 addresses contained in the SAN fields of a certificate
142- // To support IPv6 we download the certificate on the first connect and manually verify our nameserver IPv6 IP
143- // is listed in the SAN fields. We then construct a Browser instance with verify_peer_name set to false but with the peer_fingerprint set to our verified certificate.
144- // This doesn't always work because the server may use different front end certificates (SIGH!)
145- $ address = str_replace ('https:// ' , 'tls:// ' , $ this ->nameserver );
146- $ connector = new Connector ([
147- 'tcp ' => [
148- 'tcp_nodelay ' => true ,
149- ],
150- 'tls ' => [
151- 'verify_peer_name ' => false ,
152- 'capture_peer_cert ' => true
153- ],
154- 'dns ' => false ,
155- ], $ this ->loop );
156- $ connector ->connect ($ address )->then (function (ConnectionInterface $ connection ) use ($ deferred ) {
157- $ response = stream_context_get_params ($ connection ->stream ); //Using @internal stream
158- $ connection ->end ();
159- $ certificatePem = $ response ['options ' ]['ssl ' ]['peer_certificate ' ];
160-
161- $ certificateFields = openssl_x509_parse ($ certificatePem );
162- $ additionalDomains = explode (', ' , $ certificateFields ['extensions ' ]['subjectAltName ' ] ?? '' );
163-
164- $ ip = inet_pton (trim (parse_url ($ this ->nameserver , PHP_URL_HOST ), '[] ' ));
165- if ($ ip !== false ) {
166- foreach ($ additionalDomains as $ subAltName ) {
167- $ subAltName = trim (strtolower ($ subAltName ));
168- if (str_starts_with ($ subAltName , 'ip address: ' )) {
169- $ compare = inet_pton (str_replace ('ip address: ' , '' , $ subAltName ));
170- if ($ compare === $ ip ) {
171- $ fingerprint = openssl_x509_fingerprint ($ certificatePem , self ::FINGERPRINT_HASH_METHOD );
172- $ browser = (new Browser (new Connector ([
173- 'tcp ' => [
174- 'tcp_nodelay ' => true ,
175- ],
176- 'tls ' => [
177- 'verify_peer_name ' => false ,
178- 'peer_fingerprint ' =>[
179- self ::FINGERPRINT_HASH_METHOD => $ fingerprint ,
180- ],
181- ],
182- ], $ this ->loop ), $ this ->loop ));
183- $ deferred ->resolve ($ browser );
184- return ;
185- }
186- }
187- }
188- }
189- $ deferred ->reject (new RuntimeException ('IPv6 IP Address Connection Failed. Unable to Validate Peer Certificate ' ));
190-
191- }, function ($ ex ) use ($ deferred ) {
192- $ deferred ->reject (new RuntimeException ('IPv6 IP Address Connection Failed. ' . $ ex ->getMessage ()));
193- });
150+ if ($ this ->ipv6address && self ::NEED_GH9356_IPV6_WORKAROUND ) {
151+ $ this ->initialiseIPv6Workaround ($ deferred );
194152 } else {
195153 $ browser = (new Browser (new Connector ([
196154 'tcp ' => [
@@ -203,4 +161,60 @@ private function getBrowser() : Promise\PromiseInterface {
203161 }
204162 return $ this ->browserResolution ;
205163 }
164+
165+ protected function initialiseIPv6Workaround (Deferred $ deferred ) : void {
166+ // Some versions of PHP do not validate IPv6 addresses contained in the SAN fields of a certificate
167+ // To support IPv6 we download the certificate on the first connect and manually verify our nameserver IPv6 IP
168+ // is listed in the SAN fields. We then construct a Browser instance with verify_peer_name set to false but with the peer_fingerprint set to our verified certificate.
169+ // This doesn't always work because the server may use different front end certificates (SIGH!)
170+ $ address = str_replace ('https:// ' , 'tls:// ' , $ this ->nameserver );
171+ $ connector = new Connector ([
172+ 'tcp ' => [
173+ 'tcp_nodelay ' => true ,
174+ ],
175+ 'tls ' => [
176+ 'verify_peer_name ' => false ,
177+ 'capture_peer_cert ' => true
178+ ],
179+ 'dns ' => false ,
180+ ], $ this ->loop );
181+ $ connector ->connect ($ address )->then (function (ConnectionInterface $ connection ) use ($ deferred ) {
182+ $ response = stream_context_get_params ($ connection ->stream ); //Using @internal stream
183+ $ connection ->end ();
184+ $ certificatePem = $ response ['options ' ]['ssl ' ]['peer_certificate ' ];
185+
186+ $ certificateFields = openssl_x509_parse ($ certificatePem );
187+ $ additionalDomains = explode (', ' , $ certificateFields ['extensions ' ]['subjectAltName ' ] ?? '' );
188+
189+ $ ip = inet_pton (trim (parse_url ($ this ->nameserver , PHP_URL_HOST ), '[] ' ));
190+ if ($ ip !== false ) {
191+ foreach ($ additionalDomains as $ subAltName ) {
192+ $ subAltName = trim (strtolower ($ subAltName ));
193+ if (str_starts_with ($ subAltName , 'ip address: ' )) {
194+ $ compare = inet_pton (str_replace ('ip address: ' , '' , $ subAltName ));
195+ if ($ compare === $ ip ) {
196+ $ fingerprint = openssl_x509_fingerprint ($ certificatePem , self ::FINGERPRINT_HASH_METHOD );
197+ $ browser = (new Browser (new Connector ([
198+ 'tcp ' => [
199+ 'tcp_nodelay ' => true ,
200+ ],
201+ 'tls ' => [
202+ 'verify_peer_name ' => false ,
203+ 'peer_fingerprint ' =>[
204+ self ::FINGERPRINT_HASH_METHOD => $ fingerprint ,
205+ ],
206+ ],
207+ ], $ this ->loop ), $ this ->loop ));
208+ $ deferred ->resolve ($ browser );
209+ return ;
210+ }
211+ }
212+ }
213+ }
214+ $ deferred ->reject (new RuntimeException ('IPv6 IP Address Connection Failed. Unable to Validate Peer Certificate ' ));
215+
216+ }, function ($ ex ) use ($ deferred ) {
217+ $ deferred ->reject (new RuntimeException ('IPv6 IP Address Connection Failed. ' . $ ex ->getMessage ()));
218+ });
219+ }
206220}
0 commit comments