-
Notifications
You must be signed in to change notification settings - Fork 85
Add wp activitypub fetch CLI command
#2906
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
8698629
Add `wp activitypub fetch` CLI command.
pfefferle 689d5b7
Address code review feedback.
pfefferle 39e5535
Fix infinite loop when forcing rfc9421 signature mode.
pfefferle 1186aad
Add changelog
matticbot 48d0830
Add double-knock signature mode.
pfefferle 465f132
Address code review feedback for fetch CLI command.
pfefferle File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| Significance: minor | ||
| Type: added | ||
|
|
||
| Add `wp activitypub fetch` CLI command for fetching remote URLs with signed HTTP requests. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,221 @@ | ||
| <?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; | ||
pfefferle marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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] ); | ||
| } | ||
| }; | ||
| } | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.