Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions commands/Jetpack_Site_Connection_Triage.php
Original file line number Diff line number Diff line change
Expand Up @@ -171,17 +171,20 @@ protected function execute( InputInterface $input, OutputInterface $output ): in
$output->writeln( 'Checking ' . $site_url . '...' );

// fetch
$options = array(
$options = array(
'http' => array(
'method' => 'GET',
'timeout' => 60,
'ignore_errors' => true,
),
);
$context = stream_context_create( $options );
$result = @file_get_contents( $site_url, false, $context );
$headers = parse_http_headers( $http_response_header );
$status_code = $headers['http_code'];
$context = stream_context_create( $options );
$result = @file_get_contents( $site_url, false, $context );
$response_headers = function_exists( 'http_get_last_response_headers' )
? ( http_get_last_response_headers() ?? array() )
: ( $http_response_header ?? array() );
$headers = parse_http_headers( $response_headers );
$status_code = $headers['http_code'] ?? '';

if ( 410 === $status_code && $this->is_staging_domain( $site_url ) ) {
// Probably deactivated
Expand All @@ -201,9 +204,12 @@ protected function execute( InputInterface $input, OutputInterface $output ): in
// Check WPCOM API for site information
$wpcom_api_url = 'https://public-api.wordpress.com/rest/v1.1/sites/' . rawurlencode( $domain );
$wpcom_result = @file_get_contents( $wpcom_api_url, false, $context );
$wpcom_status = $http_response_header ? parse_http_headers( $http_response_header )['http_code'] : '';
$wpcom_headers = function_exists( 'http_get_last_response_headers' )
? ( http_get_last_response_headers() ?? array() )
: ( $http_response_header ?? array() );
$wpcom_status = $wpcom_headers ? ( parse_http_headers( $wpcom_headers )['http_code'] ?? '' ) : '';

if ( '400' === $wpcom_status ) {
if ( 400 === $wpcom_status ) {
$notes = 'Site is not connected to WordPress.com. If site loads, it may have moved hosts.';
} else {
$wpcom_data = json_decode( $wpcom_result, true );
Expand Down
83 changes: 70 additions & 13 deletions commands/Pressable_Site_Shell_Open.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,37 @@ protected function initialize( InputInterface $input, OutputInterface $output ):
protected function execute( InputInterface $input, OutputInterface $output ): int {
$output->writeln( "<fg=magenta;options=bold>Opening an interactive $this->shell_type shell for {$this->site->displayName} (ID {$this->site->id}, URL {$this->site->url}) as $this->email.</>" );

// Retrieve the SFTP user for the current user.
$sftp_user = get_pressable_site_sftp_user( $this->site->id, $this->email );
// Retrieve the SFTP user for the current user, creating it if it does not exist yet.
$user_was_created = false;
$sftp_user = $this->find_site_sftp_user();
if ( \is_null( $sftp_user ) ) {
$output->writeln( "<comment>Could not find a Pressable SFTP user with the email $this->email on {$this->site->displayName}. Creating...</comment>", OutputInterface::VERBOSITY_VERBOSE );
$output->writeln( "<comment>No Pressable SFTP user with the email $this->email on {$this->site->displayName}. Creating...</comment>" );

$sftp_user = create_pressable_site_collaborator( $this->site->id, $this->email );
if ( \is_null( $sftp_user ) ) {
if ( \is_null( create_pressable_site_collaborator( $this->site->id, $this->email ) ) ) {
$output->writeln( "<error>Could not create a Pressable SFTP user with the email $this->email on {$this->site->displayName}.</>" );
return Command::FAILURE;
}

// SFTP users are different from collaborator users. We need to query the API again to get the SFTP user.
$sftp_user = get_pressable_site_sftp_user( $this->site->id, $this->email );
// Creating the collaborator provisions the SFTP user asynchronously on Pressable's side, so it is
// not returned by the API right away. Poll until it shows up - this is what a manual re-run of the
// command was implicitly relying on before.
$output->writeln( '<comment>Waiting for the new SFTP user to be provisioned...</comment>' );
for ( $attempt = 1; $attempt <= 6; $attempt++ ) {
$sftp_user = $this->find_site_sftp_user();
if ( ! \is_null( $sftp_user ) ) {
break;
}

$output->writeln( "<comment>Not ready yet (attempt $attempt). Retrying in 5 seconds...</comment>", OutputInterface::VERBOSITY_VERBOSE );
\sleep( 5 );
}

if ( \is_null( $sftp_user ) ) {
$output->writeln( "<error>The SFTP user for $this->email on {$this->site->displayName} was created but did not become available in time. Please try again in a minute.</>" );
return Command::FAILURE;
}

$user_was_created = true;
}

// WPCOMSP users are logged-in through AutoProxxy, but for everyone else we must first reset their password and display it.
Expand All @@ -105,19 +123,58 @@ protected function execute( InputInterface $input, OutputInterface $output ): in
// Call the system SSH/SFTP application.
$ssh_host = $sftp_user->username . '@' . \Pressable_Connection_Helper::SSH_HOST;

$output->writeln( "<comment>Connecting to $ssh_host...</comment>", OutputInterface::VERBOSITY_VERBOSE );
if ( ! \is_null( \passthru( "$this->shell_type $ssh_host", $result_code ) ) ) {
$output->writeln( "<error>Could not open an interactive $this->shell_type shell. Error code: $result_code</error>" );
return Command::FAILURE;
}
// After a collaborator is created, Pressable still needs time to propagate the new user's access to the
// SSH gateway (ssh.atomicsites.net). Until that finishes the gateway closes the connection (exit code
// 255) even though the SFTP user already exists in the API. So for a freshly created user we retry with
// a back-off for up to ~2 minutes. Existing users get a single attempt, so a genuine failure (e.g. a
// wrong password) does not hang.
$retry_delays = $user_was_created ? array( 5, 10, 15, 20, 20, 20, 30 ) : array();
$attempt = 0;

do {
$output->writeln( "<comment>Connecting to $ssh_host...</comment>", OutputInterface::VERBOSITY_VERBOSE );

\passthru( "$this->shell_type $ssh_host", $result_code );

// 255 is the SSH/SFTP "connection or authentication error" exit code. Any other code comes from the
// interactive session itself (e.g. the user's last command), so it must not trigger a retry.
if ( 255 !== $result_code ) {
return Command::SUCCESS;
}

return Command::SUCCESS;
$delay = $retry_delays[ $attempt++ ] ?? null;
if ( null !== $delay ) {
$output->writeln( "<comment>The SSH gateway refused the connection - the new user's access may still be provisioning. Retrying in {$delay}s (Ctrl+C to abort)...</comment>" );
\sleep( $delay );
}
} while ( null !== $delay );

$output->writeln( "<error>Could not open an interactive $this->shell_type shell (exit code $result_code). The SFTP user exists but the SSH gateway is still refusing the connection; for a newly created user this usually clears within a couple of minutes. Please try again shortly.</error>" );
return Command::FAILURE;
}

// endregion

// region HELPERS

/**
* Returns the site's SFTP user matching the current email, or null if none exists yet.
*
* Uses the list endpoint and matches locally rather than the single-user lookup: the latter logs an
* API error on every miss, which is noisy while polling for a freshly provisioned user.
*
* @return \stdClass|null
*/
private function find_site_sftp_user(): ?\stdClass {
foreach ( get_pressable_site_sftp_users( $this->site->id ) ?? array() as $sftp_user ) {
if ( 0 === \strcasecmp( $sftp_user->email ?? '', $this->email ) ) {
return $sftp_user;
}
}

return null;
}

/**
* Prompts the user for a site.
*
Expand Down
8 changes: 7 additions & 1 deletion includes/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,14 @@ function get_remote_content( string $url, array $headers = array(), string $meth
}
}

// On PHP 8.4+ the locally scoped $http_response_header variable is deprecated; use http_get_last_response_headers() instead.
// The short-circuit keeps PHP < 8.4 working without reading (and thus deprecating) $http_response_header on 8.4+.
$response_headers = function_exists( 'http_get_last_response_headers' )
? ( http_get_last_response_headers() ?? array() )
: ( $http_response_header ?? array() );

return array(
'headers' => parse_http_headers( $http_response_header ),
'headers' => parse_http_headers( $response_headers ),
'body' => $result,
);
}
Expand Down
Loading