Skip to content

Commit

Permalink
Merge pull request #138 from woocommerce/24-03/windows-ssl
Browse files Browse the repository at this point in the history
Windows SSL
  • Loading branch information
Luc45 authored Mar 5, 2024
2 parents 72d145d + 024dd2a commit e1a592d
Show file tree
Hide file tree
Showing 4 changed files with 213 additions and 8 deletions.
16 changes: 8 additions & 8 deletions .github/workflows/qit-environment-test-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ on:
workflow_dispatch:

jobs:
environment_tests:
runs-on: ubuntu-20.04
env:
NO_COLOR: 1
QIT_DISABLE_ONBOARDING: yes
steps:
- name: Checkout code
uses: actions/checkout@v4
environment_tests:
runs-on: ubuntu-20.04
env:
NO_COLOR: 1
QIT_DISABLE_ONBOARDING: yes
steps:
- name: Checkout code
uses: actions/checkout@v4
78 changes: 78 additions & 0 deletions .github/workflows/qit-windows.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: QIT Windows

on:
push:
branches:
- trunk
# Manually
workflow_dispatch:

jobs:
qit_windows:
runs-on: windows-latest
strategy:
matrix:
php: [ 7.2, 7.3, 7.4, 8.0, 8.1, 8.2, 8.3 ]
env:
NO_COLOR: 1
QIT_DISABLE_ONBOARDING: yes
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: curl, zip
coverage: none

- name: Composer install
working-directory: src
run: composer install

- name: Enable dev mode
working-directory: src
run: php qit-cli.php dev

- name: Run SSL connection without CA file fallback Test
working-directory: src
env:
OPENSSL_CONF: ''
run: |
php -d openssl.cafile='' -d curl.cainfo='' qit-cli.php sync -vvv
if ($LASTEXITCODE -ne 0) {
Write-Host "Test passed: SSL connection failed as expected"
$LASTEXITCODE = 0
} else {
Write-Host "Test failed: SSL connection did not fail as expected"
exit 1
}
- name: Run SSL connection with CA file fallback Test (Cache miss)
working-directory: src
env:
QIT_WINDOWS_DOWNLOAD_CA: yes
OPENSSL_CONF: ''
run: |
php -d openssl.cafile='' -d curl.cainfo='' qit-cli.php sync -vvv
if ($LASTEXITCODE -eq 0) {
Write-Host "Test passed: SSL connection succeeded"
} else {
Write-Host "Test failed: SSL connection did not succeed"
exit 1
}
- name: Run SSL connection with CA file fallback Test (Cache hit)
working-directory: src
env:
QIT_WINDOWS_DOWNLOAD_CA: yes
OPENSSL_CONF: ''
run: |
php -d openssl.cafile='' -d curl.cainfo='' qit-cli.php sync -vvv
if ($LASTEXITCODE -eq 0) {
Write-Host "Test passed: SSL connection succeeded"
} else {
Write-Host "Test failed: SSL connection did not succeed"
exit 1
}
Binary file modified qit
Binary file not shown.
127 changes: 127 additions & 0 deletions src/src/RequestBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

use QIT_CLI\Exceptions\DoingAutocompleteException;
use QIT_CLI\Exceptions\NetworkErrorException;
use QIT_CLI\IO\Input;
use QIT_CLI\IO\Output;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Question\ConfirmationQuestion;

class RequestBuilder {
/** @var string $url */
Expand Down Expand Up @@ -34,6 +37,11 @@ class RequestBuilder {
/** @var int */
protected $timeout_in_seconds = 15;

/**
* @var bool Whether we asked about CA file on this request.
*/
protected static $asked_ca_file_override = false;

public function __construct( string $url = '' ) {
$this->url = $url;
}
Expand Down Expand Up @@ -160,6 +168,8 @@ public function request(): string {
CURLOPT_HEADER => 1,
];

$this->maybe_set_certificate_authority_file( $curl_parameters );

if ( App::make( Output::class )->isVeryVerbose() ) {
$curl_parameters[ CURLOPT_VERBOSE ] = true;
}
Expand Down Expand Up @@ -265,6 +275,16 @@ public function request(): string {
goto retry_request; // phpcs:ignore Generic.PHP.DiscourageGoto.Found
}
} else {
// Is it an SSL error?
foreach ( [ 'ssl', 'certificate', 'issuer' ] as $keyword ) {
if ( stripos( $error_message, $keyword ) !== false ) {
$downloaded = $this->maybe_download_certificate_authority_file();
if ( $downloaded ) {
goto retry_request; // phpcs:ignore Generic.PHP.DiscourageGoto.Found
}
break;
}
}
if ( $this->retry > 0 ) {
$this->retry --;
App::make( Output::class )->writeln( sprintf( '<comment>Request failed... Retrying (HTTP Status Code %s)</comment>', $response_status_code ) );
Expand All @@ -289,6 +309,113 @@ public function request(): string {
return $body;
}

/**
* @param array<int,scalar> $curl_parameters
*
* @return void
*/
protected function maybe_set_certificate_authority_file( array &$curl_parameters ) {
// Early bail: We only do this for Windows.
if ( ! is_windows() ) {
return;
}

$cached_ca_filepath = App::make( Environment::class )->get_cache()->get( 'ca_filepath' );

// Cache hit.
if ( $cached_ca_filepath !== null && file_exists( $cached_ca_filepath ) ) {
$curl_parameters[ CURLOPT_CAINFO ] = $cached_ca_filepath;
}
}

/**
* @return bool Whether it downloaded the CA file or not.
*/
protected function maybe_download_certificate_authority_file(): bool {
$output = App::make( Output::class );
// Early bail: We only do this for Windows.
if ( ! is_windows() ) {
if ( $output->isVerbose() ) {
$output->writeln( 'Skipping certificate authority file check. Not running on Windows.' );
}

return false;
}

if ( $output->isVerbose() ) {
$output->writeln( 'Checking if we need to download the certificate authority file...' );
}

$cached_ca_filepath = App::make( Environment::class )->get_cache()->get( 'ca_filepath' );

// Cache hit.
if ( $cached_ca_filepath !== null && file_exists( $cached_ca_filepath ) ) {
return false;
}

if ( $output->isVerbose() ) {
$output->writeln( 'No cached certificate authority file found.' );
}

if ( self::$asked_ca_file_override ) {
if ( $output->isVerbose() ) {
$output->writeln( 'Skipping certificate authority file check. Already asked.' );
}

return false;
}

self::$asked_ca_file_override = true;

// Ask the user if he wants us to solve it for them.
$input = App::make( Input::class );

$helper = App::make( QuestionHelper::class );
$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 );

if ( getenv( 'QIT_WINDOWS_DOWNLOAD_CA' ) !== 'yes' && ( ! $input->isInteractive() || ! $helper->ask( $input, $output, $question ) ) ) {
if ( $output->isVerbose() ) {
$output->writeln( 'Skipping certificate authority file download.' );
}

return false;
}

if ( $output->isVerbose() ) {
$output->writeln( 'Downloading certificate authority file...' );
}

// Download it to QIT Config Dir and save it in the cache.
$local_ca_file = Config::get_qit_dir() . 'cacert.pem';

if ( ! file_exists( $local_ca_file ) ) {
$remote_ca_file_contents = @file_get_contents( 'http://curl.se/ca/cacert.pem' );

if ( empty( $remote_ca_file_contents ) ) {
$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>" );

return false;
}

if ( ! file_put_contents( $local_ca_file, $remote_ca_file_contents ) ) {
$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>" );

return false;
}
clearstatcache( true, $local_ca_file );
}

if ( $output->isVerbose() ) {
$output->writeln( 'Certificate authority file downloaded and saved.' );
}

$year_in_seconds = 60 * 60 * 24 * 365;

App::make( Environment::class )->get_cache()->set( 'ca_filepath', $local_ca_file, $year_in_seconds );

return true;
}

protected function wait_after_429( string $headers, int $max_wait = 60 ): int {
$retry_after = null;

Expand Down

0 comments on commit e1a592d

Please sign in to comment.