4
4
5
5
use QIT_CLI \Exceptions \DoingAutocompleteException ;
6
6
use QIT_CLI \Exceptions \NetworkErrorException ;
7
+ use QIT_CLI \IO \Input ;
7
8
use QIT_CLI \IO \Output ;
9
+ use Symfony \Component \Console \Helper \QuestionHelper ;
10
+ use Symfony \Component \Console \Question \ConfirmationQuestion ;
8
11
9
12
class RequestBuilder {
10
13
/** @var string $url */
@@ -34,6 +37,11 @@ class RequestBuilder {
34
37
/** @var int */
35
38
protected $ timeout_in_seconds = 15 ;
36
39
40
+ /**
41
+ * @var bool Whether we asked about CA file on this request.
42
+ */
43
+ protected static $ asked_ca_file_override = false ;
44
+
37
45
public function __construct ( string $ url = '' ) {
38
46
$ this ->url = $ url ;
39
47
}
@@ -160,6 +168,8 @@ public function request(): string {
160
168
CURLOPT_HEADER => 1 ,
161
169
];
162
170
171
+ $ this ->maybe_set_certificate_authority_file ( $ curl_parameters );
172
+
163
173
if ( App::make ( Output::class )->isVeryVerbose () ) {
164
174
$ curl_parameters [ CURLOPT_VERBOSE ] = true ;
165
175
}
@@ -265,6 +275,16 @@ public function request(): string {
265
275
goto retry_request; // phpcs:ignore Generic.PHP.DiscourageGoto.Found
266
276
}
267
277
} else {
278
+ // Is it an SSL error?
279
+ foreach ( [ 'ssl ' , 'certificate ' , 'issuer ' ] as $ keyword ) {
280
+ if ( stripos ( $ error_message , $ keyword ) !== false ) {
281
+ $ downloaded = $ this ->maybe_download_certificate_authority_file ();
282
+ if ( $ downloaded ) {
283
+ goto retry_request; // phpcs:ignore Generic.PHP.DiscourageGoto.Found
284
+ }
285
+ break ;
286
+ }
287
+ }
268
288
if ( $ this ->retry > 0 ) {
269
289
$ this ->retry --;
270
290
App::make ( Output::class )->writeln ( sprintf ( '<comment>Request failed... Retrying (HTTP Status Code %s)</comment> ' , $ response_status_code ) );
@@ -289,6 +309,113 @@ public function request(): string {
289
309
return $ body ;
290
310
}
291
311
312
+ /**
313
+ * @param array<int,scalar> $curl_parameters
314
+ *
315
+ * @return void
316
+ */
317
+ protected function maybe_set_certificate_authority_file ( array &$ curl_parameters ) {
318
+ // Early bail: We only do this for Windows.
319
+ if ( ! is_windows () ) {
320
+ return ;
321
+ }
322
+
323
+ $ cached_ca_filepath = App::make ( Environment::class )->get_cache ()->get ( 'ca_filepath ' );
324
+
325
+ // Cache hit.
326
+ if ( $ cached_ca_filepath !== null && file_exists ( $ cached_ca_filepath ) ) {
327
+ $ curl_parameters [ CURLOPT_CAINFO ] = $ cached_ca_filepath ;
328
+ }
329
+ }
330
+
331
+ /**
332
+ * @return bool Whether it downloaded the CA file or not.
333
+ */
334
+ protected function maybe_download_certificate_authority_file (): bool {
335
+ $ output = App::make ( Output::class );
336
+ // Early bail: We only do this for Windows.
337
+ if ( ! is_windows () ) {
338
+ if ( $ output ->isVerbose () ) {
339
+ $ output ->writeln ( 'Skipping certificate authority file check. Not running on Windows. ' );
340
+ }
341
+
342
+ return false ;
343
+ }
344
+
345
+ if ( $ output ->isVerbose () ) {
346
+ $ output ->writeln ( 'Checking if we need to download the certificate authority file... ' );
347
+ }
348
+
349
+ $ cached_ca_filepath = App::make ( Environment::class )->get_cache ()->get ( 'ca_filepath ' );
350
+
351
+ // Cache hit.
352
+ if ( $ cached_ca_filepath !== null && file_exists ( $ cached_ca_filepath ) ) {
353
+ return false ;
354
+ }
355
+
356
+ if ( $ output ->isVerbose () ) {
357
+ $ output ->writeln ( 'No cached certificate authority file found. ' );
358
+ }
359
+
360
+ if ( self ::$ asked_ca_file_override ) {
361
+ if ( $ output ->isVerbose () ) {
362
+ $ output ->writeln ( 'Skipping certificate authority file check. Already asked. ' );
363
+ }
364
+
365
+ return false ;
366
+ }
367
+
368
+ self ::$ asked_ca_file_override = true ;
369
+
370
+ // Ask the user if he wants us to solve it for them.
371
+ $ input = App::make ( Input::class );
372
+
373
+ $ helper = App::make ( QuestionHelper::class );
374
+ $ question = new ConfirmationQuestion ( "A QIT network request failed due to an SSL certificate issue on Windows. Would you like to download a CA file, used exclusively for QIT requests, to potentially fix this? \n Please answer [y/n]: " , false );
375
+
376
+ if ( getenv ( 'QIT_WINDOWS_DOWNLOAD_CA ' ) !== 'yes ' && ( ! $ input ->isInteractive () || ! $ helper ->ask ( $ input , $ output , $ question ) ) ) {
377
+ if ( $ output ->isVerbose () ) {
378
+ $ output ->writeln ( 'Skipping certificate authority file download. ' );
379
+ }
380
+
381
+ return false ;
382
+ }
383
+
384
+ if ( $ output ->isVerbose () ) {
385
+ $ output ->writeln ( 'Downloading certificate authority file... ' );
386
+ }
387
+
388
+ // Download it to QIT Config Dir and save it in the cache.
389
+ $ local_ca_file = Config::get_qit_dir () . 'cacert.pem ' ;
390
+
391
+ if ( ! file_exists ( $ local_ca_file ) ) {
392
+ $ remote_ca_file_contents = @file_get_contents ( 'http://curl.se/ca/cacert.pem ' );
393
+
394
+ if ( empty ( $ remote_ca_file_contents ) ) {
395
+ $ output ->writeln ( "<error>Could not download the certificate authority file. Please download it manually from http://curl.se/ca/cacert.pem and place it in $ local_ca_file</error> " );
396
+
397
+ return false ;
398
+ }
399
+
400
+ if ( ! file_put_contents ( $ local_ca_file , $ remote_ca_file_contents ) ) {
401
+ $ output ->writeln ( "<error>Could not write the certificate authority file. Please download it manually from http://curl.se/ca/cacert.pem and place it in $ local_ca_file<error> " );
402
+
403
+ return false ;
404
+ }
405
+ clearstatcache ( true , $ local_ca_file );
406
+ }
407
+
408
+ if ( $ output ->isVerbose () ) {
409
+ $ output ->writeln ( 'Certificate authority file downloaded and saved. ' );
410
+ }
411
+
412
+ $ year_in_seconds = 60 * 60 * 24 * 365 ;
413
+
414
+ App::make ( Environment::class )->get_cache ()->set ( 'ca_filepath ' , $ local_ca_file , $ year_in_seconds );
415
+
416
+ return true ;
417
+ }
418
+
292
419
protected function wait_after_429 ( string $ headers , int $ max_wait = 60 ): int {
293
420
$ retry_after = null ;
294
421
0 commit comments