Skip to content

Commit e1a592d

Browse files
authored
Merge pull request #138 from woocommerce/24-03/windows-ssl
Windows SSL
2 parents 72d145d + 024dd2a commit e1a592d

File tree

4 files changed

+213
-8
lines changed

4 files changed

+213
-8
lines changed

.github/workflows/qit-environment-test-linux.yml

+8-8
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ on:
55
workflow_dispatch:
66

77
jobs:
8-
environment_tests:
9-
runs-on: ubuntu-20.04
10-
env:
11-
NO_COLOR: 1
12-
QIT_DISABLE_ONBOARDING: yes
13-
steps:
14-
- name: Checkout code
15-
uses: actions/checkout@v4
8+
environment_tests:
9+
runs-on: ubuntu-20.04
10+
env:
11+
NO_COLOR: 1
12+
QIT_DISABLE_ONBOARDING: yes
13+
steps:
14+
- name: Checkout code
15+
uses: actions/checkout@v4

.github/workflows/qit-windows.yml

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
name: QIT Windows
2+
3+
on:
4+
push:
5+
branches:
6+
- trunk
7+
# Manually
8+
workflow_dispatch:
9+
10+
jobs:
11+
qit_windows:
12+
runs-on: windows-latest
13+
strategy:
14+
matrix:
15+
php: [ 7.2, 7.3, 7.4, 8.0, 8.1, 8.2, 8.3 ]
16+
env:
17+
NO_COLOR: 1
18+
QIT_DISABLE_ONBOARDING: yes
19+
steps:
20+
- name: Checkout code
21+
uses: actions/checkout@v4
22+
23+
- name: Set up PHP ${{ matrix.php }}
24+
uses: shivammathur/setup-php@v2
25+
with:
26+
php-version: ${{ matrix.php }}
27+
extensions: curl, zip
28+
coverage: none
29+
30+
- name: Composer install
31+
working-directory: src
32+
run: composer install
33+
34+
- name: Enable dev mode
35+
working-directory: src
36+
run: php qit-cli.php dev
37+
38+
- name: Run SSL connection without CA file fallback Test
39+
working-directory: src
40+
env:
41+
OPENSSL_CONF: ''
42+
run: |
43+
php -d openssl.cafile='' -d curl.cainfo='' qit-cli.php sync -vvv
44+
if ($LASTEXITCODE -ne 0) {
45+
Write-Host "Test passed: SSL connection failed as expected"
46+
$LASTEXITCODE = 0
47+
} else {
48+
Write-Host "Test failed: SSL connection did not fail as expected"
49+
exit 1
50+
}
51+
52+
- name: Run SSL connection with CA file fallback Test (Cache miss)
53+
working-directory: src
54+
env:
55+
QIT_WINDOWS_DOWNLOAD_CA: yes
56+
OPENSSL_CONF: ''
57+
run: |
58+
php -d openssl.cafile='' -d curl.cainfo='' qit-cli.php sync -vvv
59+
if ($LASTEXITCODE -eq 0) {
60+
Write-Host "Test passed: SSL connection succeeded"
61+
} else {
62+
Write-Host "Test failed: SSL connection did not succeed"
63+
exit 1
64+
}
65+
66+
- name: Run SSL connection with CA file fallback Test (Cache hit)
67+
working-directory: src
68+
env:
69+
QIT_WINDOWS_DOWNLOAD_CA: yes
70+
OPENSSL_CONF: ''
71+
run: |
72+
php -d openssl.cafile='' -d curl.cainfo='' qit-cli.php sync -vvv
73+
if ($LASTEXITCODE -eq 0) {
74+
Write-Host "Test passed: SSL connection succeeded"
75+
} else {
76+
Write-Host "Test failed: SSL connection did not succeed"
77+
exit 1
78+
}

qit

4.13 KB
Binary file not shown.

src/src/RequestBuilder.php

+127
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
use QIT_CLI\Exceptions\DoingAutocompleteException;
66
use QIT_CLI\Exceptions\NetworkErrorException;
7+
use QIT_CLI\IO\Input;
78
use QIT_CLI\IO\Output;
9+
use Symfony\Component\Console\Helper\QuestionHelper;
10+
use Symfony\Component\Console\Question\ConfirmationQuestion;
811

912
class RequestBuilder {
1013
/** @var string $url */
@@ -34,6 +37,11 @@ class RequestBuilder {
3437
/** @var int */
3538
protected $timeout_in_seconds = 15;
3639

40+
/**
41+
* @var bool Whether we asked about CA file on this request.
42+
*/
43+
protected static $asked_ca_file_override = false;
44+
3745
public function __construct( string $url = '' ) {
3846
$this->url = $url;
3947
}
@@ -160,6 +168,8 @@ public function request(): string {
160168
CURLOPT_HEADER => 1,
161169
];
162170

171+
$this->maybe_set_certificate_authority_file( $curl_parameters );
172+
163173
if ( App::make( Output::class )->isVeryVerbose() ) {
164174
$curl_parameters[ CURLOPT_VERBOSE ] = true;
165175
}
@@ -265,6 +275,16 @@ public function request(): string {
265275
goto retry_request; // phpcs:ignore Generic.PHP.DiscourageGoto.Found
266276
}
267277
} 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+
}
268288
if ( $this->retry > 0 ) {
269289
$this->retry --;
270290
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 {
289309
return $body;
290310
}
291311

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+
292419
protected function wait_after_429( string $headers, int $max_wait = 60 ): int {
293420
$retry_after = null;
294421

0 commit comments

Comments
 (0)