Files
laipower/wp-content/plugins/activitypub/includes/cli/class-fetch-command.php

222 lines
6.5 KiB
PHP

<?php
/**
* Fetch CLI Command.
*
* @package Activitypub
*/
namespace Activitypub\Cli;
use Activitypub\Http;
use Activitypub\Signature;
use Activitypub\Signature\Http_Message_Signature;
/**
* Fetch a remote ActivityPub URL with signed HTTP requests.
*
* Useful for debugging HTTP Signatures and federation issues.
* Signs requests as the application actor by default.
*
* @package Activitypub
*/
class Fetch_Command extends \WP_CLI_Command {
/**
* Fetch a remote ActivityPub URL with a signed HTTP request.
*
* Signs the request as the application actor and displays the response.
* Supports switching between signature modes for debugging.
*
* ## OPTIONS
*
* <url>
* : The URL to fetch.
*
* [--signature=<mode>]
* : Signature mode: default (plugin-configured), draft-cavage, rfc9421, double-knock, or none.
* ---
* default: default
* options:
* - default
* - draft-cavage
* - rfc9421
* - double-knock
* - none
* ---
*
* [--raw]
* : Output the raw response body without formatting.
*
* [--include-headers]
* : Show response headers alongside the body.
*
* ## EXAMPLES
*
* # Fetch an actor profile with default signature
* $ wp activitypub fetch https://mastodon.social/@Gargron
*
* # Fetch with RFC 9421 signature
* $ wp activitypub fetch https://mastodon.social/@Gargron --signature=rfc9421
*
* # Fetch with Draft Cavage signature
* $ wp activitypub fetch https://mastodon.social/@Gargron --signature=draft-cavage
*
* # Fetch with double-knock (RFC 9421 first, Draft Cavage fallback on 4xx)
* $ wp activitypub fetch https://mastodon.social/@Gargron --signature=double-knock
*
* # Fetch without signature
* $ wp activitypub fetch https://mastodon.social/@Gargron --signature=none
*
* # Show response headers
* $ wp activitypub fetch https://mastodon.social/@Gargron --include-headers
*
* # Output raw response body
* $ wp activitypub fetch https://mastodon.social/@Gargron --raw
*
* @param array $args The positional arguments.
* @param array $assoc_args The associative arguments.
*/
public function __invoke( $args, $assoc_args ) {
$url = $args[0];
$signature_mode = \WP_CLI\Utils\get_flag_value( $assoc_args, 'signature', 'default' );
$raw = \WP_CLI\Utils\get_flag_value( $assoc_args, 'raw', false );
$include_headers = \WP_CLI\Utils\get_flag_value( $assoc_args, 'include-headers', false );
\WP_CLI::log( \sprintf( 'Fetching: %s', $url ) );
\WP_CLI::log( \sprintf( 'Signature mode: %s', $signature_mode ) );
$get_args = array();
$cleanup = $this->apply_signature_mode( $signature_mode, $get_args );
$response = Http::get( $url, $get_args, false );
$cleanup();
if ( \is_wp_error( $response ) ) {
\WP_CLI::error( \sprintf( 'Request failed: %s (Error code: %s).', $response->get_error_message(), $response->get_error_code() ) );
}
$code = \wp_remote_retrieve_response_code( $response );
\WP_CLI::log( \sprintf( 'Response code: %d', $code ) );
\WP_CLI::log( '' );
// Show response headers if requested.
if ( $include_headers ) {
$headers = \wp_remote_retrieve_headers( $response );
\WP_CLI::log( '--- Response Headers ---' );
foreach ( $headers as $name => $value ) {
\WP_CLI::log( \sprintf( '%s: %s', $name, $value ) );
}
\WP_CLI::log( '' );
}
$body = \wp_remote_retrieve_body( $response );
// Output the body.
if ( $raw ) {
\WP_CLI::log( $body );
} else {
$data = \json_decode( $body, true );
if ( \JSON_ERROR_NONE === \json_last_error() ) {
\WP_CLI::log( \wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) );
} else {
\WP_CLI::log( $body );
}
}
}
/**
* Apply signature mode overrides via filters.
*
* For rfc9421, replaces the default sign_request and disables double-knock
* to avoid an infinite retry loop when the server returns 4xx.
*
* @param string $mode The signature mode.
* @param array $args The request arguments, passed by reference.
*
* @return callable Cleanup callback to restore original filters.
*/
private function apply_signature_mode( $mode, &$args ) {
$filters = array();
$restore = array();
switch ( $mode ) {
case 'default':
break;
case 'none':
$args['key_id'] = null;
$args['private_key'] = null;
break;
case 'rfc9421':
case 'double-knock':
// Replace default signing to force RFC 9421. For rfc9421 mode,
// also disable double-knock to prevent an infinite retry loop.
// For double-knock mode, keep it active but skip re-signing on retry.
$removed_sign_request = \remove_filter( 'http_request_args', array( Signature::class, 'sign_request' ), 0 );
$is_double_knock = 'double-knock' === $mode;
$removed_double_knock = false;
if ( ! $is_double_knock ) {
$removed_double_knock = \remove_filter( 'http_response', array( Signature::class, 'maybe_double_knock' ), 10 );
}
$forced_signer = function ( $request_args, $url ) use ( $is_double_knock ) {
if ( ! isset( $request_args['key_id'], $request_args['private_key'] ) ) {
return $request_args;
}
// In double-knock mode, skip if already signed (retry from maybe_double_knock).
if ( $is_double_knock && ! empty( $request_args['headers']['Signature'] ) ) {
return $request_args;
}
return ( new Http_Message_Signature() )->sign( $request_args, $url );
};
\add_filter( 'http_request_args', $forced_signer, 0, 2 );
$filters[] = array( 'http_request_args', $forced_signer, 0 );
if ( $removed_sign_request ) {
$restore[] = array( 'http_request_args', array( Signature::class, 'sign_request' ), 0, 2 );
}
if ( $removed_double_knock ) {
$restore[] = array( 'http_response', array( Signature::class, 'maybe_double_knock' ), 10, 3 );
}
break;
case 'draft-cavage':
$force_cavage = function () {
return '0';
};
\add_filter( 'pre_option_activitypub_rfc9421_signature', $force_cavage );
$filters[] = array( 'pre_option_activitypub_rfc9421_signature', $force_cavage );
break;
default:
\WP_CLI::error(
\sprintf(
'Invalid signature mode "%s". Allowed modes: default, draft-cavage, rfc9421, double-knock, none.',
$mode
)
);
}
return function () use ( $filters, $restore ) {
foreach ( $filters as $filter ) {
\remove_filter( $filter[0], $filter[1], $filter[2] ?? 10 );
}
foreach ( $restore as $filter ) {
\add_filter( $filter[0], $filter[1], $filter[2], $filter[3] );
}
};
}
}