From 7bcd586eae0eb31ab719517d3122f89a29f57b2e Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Feb 2020 11:05:17 +0100 Subject: [PATCH 1/6] fix `get_remote_metadata_by_actor` --- includes/functions.php | 54 +++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index 30e91a0..2cd3607 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -48,6 +48,32 @@ function safe_remote_post( $url, $body, $user_id ) { return $response; } +function safe_remote_get( $url, $user_id ) { + $date = \gmdate( 'D, d M Y H:i:s T' ); + $signature = \Activitypub\Signature::generate_signature( $user_id, $url, $date ); + + $wp_version = \get_bloginfo( 'version' ); + $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); + $args = array( + 'timeout' => 100, + 'limit_response_size' => 1048576, + 'redirection' => 3, + 'user-agent' => "$user_agent; ActivityPub", + 'headers' => array( + 'Accept' => 'application/activity+json', + 'Content-Type' => 'application/activity+json', + 'Signature' => $signature, + 'Date' => $date, + ), + ); + + $response = \wp_safe_remote_get( $url, $args ); + + \do_action( 'activitypub_safe_remote_get_response', $response, $url, $user_id ); + + return $response; +} + /** * Returns a users WebFinger "resource" * @@ -84,32 +110,16 @@ function get_remote_metadata_by_actor( $actor ) { return new \WP_Error( 'activitypub_no_valid_actor_url', \__( 'The "actor" is no valid URL', 'activitypub' ), $actor ); } - // we just need any user to generate a request signature - $user_id = \reset( \get_users( array ( + $user = \get_users( array ( 'number' => 1, 'who' => 'authors', - 'fields' => 'ID' - ) ) ); + 'fields' => 'ID', + ) ); - $date = \gmdate( 'D, d M Y H:i:s T' ); - $signature = \Activitypub\Signature::generate_signature( $user_id, $url, $date ); + // we just need any user to generate a request signature + $user_id = \reset( $user ); - $wp_version = \get_bloginfo( 'version' ); - - $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); - $args = array( - 'timeout' => 100, - 'limit_response_size' => 1048576, - 'redirection' => 3, - 'user-agent' => "$user_agent; ActivityPub", - 'headers' => array( - 'accept' => 'application/activity+json, application/ld+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - 'Signature' => $signature, - 'Date' => $date, - ), - ); - - $response = \wp_safe_remote_get( $actor, $args ); + $response = \Activitypub\safe_remote_get( $actor, $user_id ); if ( \is_wp_error( $response ) ) { return $response; From 0d48496768faa662538af290f1daae7d919ded7b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Feb 2020 11:09:31 +0100 Subject: [PATCH 2/6] add blacklist settings --- includes/class-admin.php | 8 +++++++ includes/functions.php | 47 ++++++++++++++++++++++++++++++++++++++++ templates/settings.php | 25 +++++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/includes/class-admin.php b/includes/class-admin.php index 866f873..0dca896 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -106,6 +106,14 @@ class Admin { 'default' => array( 'post', 'pages' ), ) ); + \register_setting( + 'activitypub', 'activitypub_blacklist', array( + 'type' => 'string', + 'description' => \esc_html__( 'Block fediverse instances', 'activitypub' ), + 'show_in_rest' => true, + 'default' => 'gab.com', + ) + ); } public static function add_settings_help_tab() { diff --git a/includes/functions.php b/includes/functions.php index 2cd3607..d1b7991 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -287,3 +287,50 @@ function url_to_authorid( $url ) { return 0; } + +/** + * Get the blacklist from the WordPress options table + * + * @return array the list of blacklisted hosts + * + * @uses apply_filters() Calls 'activitypub_blacklist' filter + */ +function get_blacklist() { + $blacklist = \get_option( 'activitypub_blacklist' ); + $blacklist_hosts = \explode( PHP_EOL, $blacklist ); + + // if no values have been set, revert to the defaults + if ( ! $blacklist || ! $blacklist_hosts || ! is_array( $blacklist_hosts ) ) { + $blacklist_hosts = array( + 'gab.com', + ); + } + + // clean out any blank values + foreach ( $blacklist_hosts as $key => $value ) { + if ( empty( $value ) ) { + unset( $blacklist_hosts[ $key ] ); + } else { + $blacklist_hosts[ $key ] = \trim( $blacklist_hosts[ $key ] ); + } + } + + return \apply_filters( 'activitypub_blacklist', $blacklist_hosts ); +} + +/** + * Check if an URL is blacklisted + * + * @param string $url an URL to check + * + * @return boolean + */ +function is_blacklisted( $url ) { + foreach ( \ActivityPub\get_blacklist() as $blacklisted_host ) { + if ( \strpos( $url, $blacklisted_host ) !== false ) { + return true; + } + } + + return false; +} diff --git a/templates/settings.php b/templates/settings.php index d3d69ea..ff1d0c7 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -90,6 +90,31 @@ +

+ +

+ + + + + + + + + + +
+ + + +

http:// and without www.. For example example.com.', 'activitypub' ); ?>

+
+ + + From 385aac3568fd2b0a0bb8858ed339a8333581aaaa Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Feb 2020 11:11:03 +0100 Subject: [PATCH 3/6] improve request validation and added blacklist check --- activitypub.php | 5 ++ includes/rest/class-inbox.php | 142 ++++++++++++--------------------- includes/rest/class-server.php | 31 +++++++ 3 files changed, 88 insertions(+), 90 deletions(-) create mode 100644 includes/rest/class-server.php diff --git a/activitypub.php b/activitypub.php index e0f7880..23ff78b 100644 --- a/activitypub.php +++ b/activitypub.php @@ -66,6 +66,11 @@ function init() { require_once \dirname( __FILE__ ) . '/includes/class-health-check.php'; \Activitypub\Health_Check::init(); + + require_once \dirname( __FILE__ ) . '/includes/rest/class-server.php'; + \add_filter( 'wp_rest_server_class', function() { + return '\Activitypub\Rest\Server'; + } ); } add_action( 'plugins_loaded', '\Activitypub\init' ); diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index e2772ae..3709d4e 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -14,7 +14,7 @@ class Inbox { */ public static function init() { \add_action( 'rest_api_init', array( '\Activitypub\Rest\Inbox', 'register_routes' ) ); - //\add_filter( 'rest_pre_serve_request', array( '\Activitypub\Rest\Inbox', 'serve_request' ), 11, 4 ); + \add_filter( 'rest_pre_serve_request', array( '\Activitypub\Rest\Inbox', 'serve_request' ), 11, 4 ); \add_action( 'activitypub_inbox_follow', array( '\Activitypub\Rest\Inbox', 'handle_follow' ), 10, 2 ); \add_action( 'activitypub_inbox_unfollow', array( '\Activitypub\Rest\Inbox', 'handle_unfollow' ), 10, 2 ); //\add_action( 'activitypub_inbox_like', array( '\Activitypub\Rest\Inbox', 'handle_reaction' ), 10, 2 ); @@ -36,7 +36,7 @@ class Inbox { ); \register_rest_route( - 'activitypub/1.0', '/users/(?P\d+)/inbox', array( + 'activitypub/1.0', '/users/(?P\d+)/inbox', array( array( 'methods' => \WP_REST_Server::EDITABLE, 'callback' => array( '\Activitypub\Rest\Inbox', 'user_inbox' ), @@ -61,10 +61,6 @@ class Inbox { return $served; } - if ( 'POST' !== $request->get_method() ) { - return $served; - } - $signature = $request->get_header( 'signature' ); if ( ! $signature ) { @@ -73,6 +69,7 @@ class Inbox { $headers = $request->get_headers(); + // verify signature //\Activitypub\Signature::verify_signature( $headers, $key ); return $served; @@ -86,21 +83,13 @@ class Inbox { * @return WP_REST_Response */ public static function user_inbox( $request ) { - $author_id = $request->get_param( 'id' ); + $user_id = $request->get_param( 'user_id' ); - $data = \json_decode( $request->get_body(), true ); + $data = $request->get_params(); + $type = $request->get_param( 'type' ); - if ( ! \is_array( $data ) || ! \array_key_exists( 'type', $data ) ) { - return new \WP_Error( 'rest_invalid_data', \__( 'Invalid payload', 'activitypub' ), array( 'status' => 422 ) ); - } - - $type = 'create'; - if ( ! empty( $data['type'] ) ) { - $type = \strtolower( $data['type'] ); - } - - \do_action( 'activitypub_inbox', $data, $author_id, $type ); - \do_action( "activitypub_inbox_{$type}", $data, $author_id ); + \do_action( 'activitypub_inbox', $data, $user_id, $type ); + \do_action( "activitypub_inbox_{$type}", $data, $user_id ); return new \WP_REST_Response( array(), 202 ); } @@ -113,61 +102,7 @@ class Inbox { * @return WP_Error not yet implemented */ public static function shared_inbox( $request ) { - $data = \json_decode( $request->get_body(), true ); - if ( empty( $data['to'] ) ) { - return new \WP_Error( 'rest_invalid_data', \__( 'No receiving actor set', 'activitypub' ), array( 'status' => 422 ) ); - } - - if ( \filter_var( $data['to'], \FILTER_VALIDATE_URL ) ) { - $author_id = \Activitypub\url_to_authorid( $data['to'] ); - - if ( ! $author_id ) { - return new \WP_Error( 'rest_invalid_data', \__( 'No matching user', 'activitypub' ), array( 'status' => 422 ) ); - } - } else { - // get the identifier at the left of the '@' - $parts = \explode( '@', $data['to'] ); - - if ( 3 === \count( $parts ) ) { - $username = $parts[1]; - $host = $parts[2]; - } elseif ( 2 === \count( $parts ) ) { - $username = $parts[0]; - $host = $parts[1]; - } - - if ( ! $username || ! $host ) { - return new \WP_Error( 'rest_invalid_data', \__( 'Invalid actor identifier', 'activitypub' ), array( 'status' => 422 ) ); - } - - // check domain - if ( ! \wp_parse_url( \home_url(), \PHP_URL_HOST ) !== $host ) { - return new \WP_Error( 'rest_invalid_data', \__( 'Invalid host', 'activitypub' ), array( 'status' => 422 ) ); - } - - $author = \get_user_by( 'login', $username ); - - if ( ! $author ) { - return new \WP_Error( 'rest_invalid_data', \__( 'No matching user', 'activitypub' ), array( 'status' => 422 ) ); - } - - $author_id = $author->ID; - } - - if ( ! \is_array( $data ) || ! \array_key_exists( 'type', $data ) ) { - return new \WP_Error( 'rest_invalid_data', \__( 'Invalid payload', 'activitypub' ), array( 'status' => 422 ) ); - } - - $type = 'create'; - if ( ! empty( $data['type'] ) ) { - $type = \strtolower( $data['type'] ); - } - - \do_action( 'activitypub_inbox', $data, $author_id, $type ); - \do_action( "activitypub_inbox_{$type}", $data, $author_id ); - - return new \WP_REST_Response( array(), 202 ); } /** @@ -182,11 +117,54 @@ class Inbox { 'type' => 'integer', ); - $params['id'] = array( + $params['user_id'] = array( 'required' => true, 'type' => 'integer', ); + $params['id'] = array( + 'required' => true, + 'type' => 'string', + 'validate_callback' => function( $param, $request, $key ) { + if ( ! is_string( $param ) ) { + $param = $param['id']; + } + return ! \Activitypub\is_blacklisted( $param ); + }, + 'sanitize_callback' => 'esc_url_raw', + ); + + $params['actor'] = array( + 'required' => true, + 'type' => array( 'object', 'string' ), + 'validate_callback' => function( $param, $request, $key ) { + if ( ! is_string( $param ) ) { + $param = $param['id']; + } + return ! \Activitypub\is_blacklisted( $param ); + }, + 'sanitize_callback' => function( $param, $request, $key ) { + if ( ! is_string( $param ) ) { + $param = $param['id']; + } + return \esc_url_raw( $param ); + }, + ); + + $params['type'] = array( + 'required' => true, + 'type' => 'enum', + 'enum' => array( 'Create' ), + 'sanitize_callback' => function( $param, $request, $key ) { + return \strtolower( $param ); + }, + ); + + $params['object'] = array( + 'required' => true, + 'type' => 'object', + ); + return $params; } @@ -197,10 +175,6 @@ class Inbox { * @param int $user_id The id of the local blog-user */ public static function handle_follow( $object, $user_id ) { - if ( ! \array_key_exists( 'actor', $object ) ) { - return new \WP_Error( 'activitypub_no_actor', __( 'No "Actor" found', 'activitypub' ) ); - } - // save follower \Activitypub\Peer\Followers::add_follower( $object['actor'], $user_id ); @@ -226,10 +200,6 @@ class Inbox { * @param int $user_id The id of the local blog-user */ public static function handle_unfollow( $object, $user_id ) { - if ( ! \array_key_exists( 'actor', $object ) ) { - return new \WP_Error( 'activitypub_no_actor', \__( 'No "Actor" found', 'activitypub' ) ); - } - \Activitypub\Peer\Followers::remove_follower( $object['actor'], $user_id ); } @@ -240,10 +210,6 @@ class Inbox { * @param int $user_id The id of the local blog-user */ public static function handle_reaction( $object, $user_id ) { - if ( ! \array_key_exists( 'actor', $object ) ) { - return new \WP_Error( 'activitypub_no_actor', \__( 'No "Actor" found', 'activitypub' ) ); - } - $meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] ); $commentdata = array( @@ -277,10 +243,6 @@ class Inbox { * @param int $user_id The id of the local blog-user */ public static function handle_create( $object, $user_id ) { - if ( ! \array_key_exists( 'actor', $object ) ) { - return new \WP_Error( 'activitypub_no_actor', __( 'No "Actor" found', 'activitypub' ) ); - } - $meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] ); $commentdata = array( diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php new file mode 100644 index 0000000..79926fc --- /dev/null +++ b/includes/rest/class-server.php @@ -0,0 +1,31 @@ +get_content_type(); + + // check for content-sub-types like 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' + if ( preg_match( '/application\/([a-zA-Z+_-]+\+)json/', $content_type['value'] ) ) { + $request->set_header( 'Content-Type', 'application/json' ); + } + + // make request filterable + $request = apply_filters( 'activitypub_pre_dispatch_request', $request ); + + return parent::dispatch( $request ); + } +} From 3f59e8fe97b8e2b9da9d46babeca3a6117f94793 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Feb 2020 11:11:12 +0100 Subject: [PATCH 4/6] update language file --- languages/activitypub.pot | 80 +++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/languages/activitypub.pot b/languages/activitypub.pot index 11bac29..4c0abfb 100644 --- a/languages/activitypub.pot +++ b/languages/activitypub.pot @@ -5,7 +5,7 @@ msgstr "" "Project-Id-Version: ActivityPub 0.9.1\n" "Report-Msgid-Bugs-To: " "https://wordpress.org/support/plugin/wordpress-activitypub\n" -"POT-Creation-Date: 2020-02-11 09:13:49+00:00\n" +"POT-Creation-Date: 2020-02-21 10:08:48+00:00\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -48,11 +48,15 @@ msgstr "" msgid "Enable ActivityPub support for post types" msgstr "" -#: includes/class-admin.php:115 +#: includes/class-admin.php:112 +msgid "Block fediverse instances" +msgstr "" + +#: includes/class-admin.php:123 msgid "Overview" msgstr "" -#: includes/class-admin.php:117 +#: includes/class-admin.php:125 msgid "" "ActivityPub is a decentralized social networking protocol based on the " "ActivityStreams 2.0 data format. ActivityPub is an official W3C recommended " @@ -62,53 +66,53 @@ msgid "" "subscribing to content." msgstr "" -#: includes/class-admin.php:122 +#: includes/class-admin.php:130 msgid "For more information:" msgstr "" -#: includes/class-admin.php:123 +#: includes/class-admin.php:131 msgid "Test Suite" msgstr "" -#: includes/class-admin.php:124 +#: includes/class-admin.php:132 msgid "W3C Spec" msgstr "" -#: includes/class-admin.php:125 +#: includes/class-admin.php:133 msgid "" "Give " "us feedback" msgstr "" -#: includes/class-admin.php:127 +#: includes/class-admin.php:135 msgid "Donate" msgstr "" -#: includes/class-admin.php:137 +#: includes/class-admin.php:145 msgid "Fediverse" msgstr "" -#: includes/functions.php:84 +#: includes/functions.php:110 msgid "The \"actor\" is no valid URL" msgstr "" -#: includes/functions.php:122 +#: includes/functions.php:132 msgid "No valid JSON data" msgstr "" -#: includes/functions.php:150 +#: includes/functions.php:160 msgid "No \"Inbox\" found" msgstr "" -#: includes/functions.php:176 +#: includes/functions.php:186 msgid "No \"Public-Key\" found" msgstr "" -#: includes/functions.php:204 +#: includes/functions.php:214 msgid "Profile identifier" msgstr "" -#: includes/functions.php:209 +#: includes/functions.php:219 #. translators: the webfinger resource msgid "Try to follow \"@%s\" in the Mastodon/Friendica search field." msgstr "" @@ -124,31 +128,6 @@ msgstr "" msgid "User not found" msgstr "" -#: includes/rest/class-inbox.php:94 includes/rest/class-inbox.php:159 -msgid "Invalid payload" -msgstr "" - -#: includes/rest/class-inbox.php:119 -msgid "No receiving actor set" -msgstr "" - -#: includes/rest/class-inbox.php:126 includes/rest/class-inbox.php:152 -msgid "No matching user" -msgstr "" - -#: includes/rest/class-inbox.php:141 -msgid "Invalid actor identifier" -msgstr "" - -#: includes/rest/class-inbox.php:146 -msgid "Invalid host" -msgstr "" - -#: includes/rest/class-inbox.php:201 includes/rest/class-inbox.php:230 -#: includes/rest/class-inbox.php:244 includes/rest/class-inbox.php:281 -msgid "No \"Actor\" found" -msgstr "" - #: includes/rest/class-webfinger.php:48 msgid "Resource is invalid" msgstr "" @@ -280,7 +259,26 @@ msgstr "" msgid "Add all tags as hashtags to the end of each activity." msgstr "" -#: templates/settings.php:99 +#: templates/settings.php:93 +msgid "Server" +msgstr "" + +#: templates/settings.php:95 +msgid "Server related settings." +msgstr "" + +#: templates/settings.php:106 +msgid "Blacklist" +msgstr "" + +#: templates/settings.php:110 +msgid "" +"A list of hosts, you want to block, one host per line. Please use only the " +"host/domain of the server you want to block, without http:// " +"and without www.. For example example.com." +msgstr "" + +#: templates/settings.php:124 msgid "" "If you like this plugin, what about a small donation?" From d0ec22bcc8565bfa9a71f8a86af272bc3da1404f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Feb 2020 11:11:27 +0100 Subject: [PATCH 5/6] fix phpcs config --- phpcs.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/phpcs.xml b/phpcs.xml index a3a5f6a..3313e50 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -4,8 +4,6 @@ ./activitypub.php ./includes/ - - *\.(inc|css|js|svg) */vendor/* */node_modules/* From 273787295a9556d95c4ba5ae39a61d98f53b7d86 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Sat, 22 Feb 2020 13:02:58 +0100 Subject: [PATCH 6/6] native function invocation --- includes/functions.php | 2 +- includes/rest/class-inbox.php | 6 +++--- includes/rest/class-server.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index d1b7991..31d29cf 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -300,7 +300,7 @@ function get_blacklist() { $blacklist_hosts = \explode( PHP_EOL, $blacklist ); // if no values have been set, revert to the defaults - if ( ! $blacklist || ! $blacklist_hosts || ! is_array( $blacklist_hosts ) ) { + if ( ! $blacklist || ! $blacklist_hosts || ! \is_array( $blacklist_hosts ) ) { $blacklist_hosts = array( 'gab.com', ); diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 3709d4e..baca644 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -126,7 +126,7 @@ class Inbox { 'required' => true, 'type' => 'string', 'validate_callback' => function( $param, $request, $key ) { - if ( ! is_string( $param ) ) { + if ( ! \is_string( $param ) ) { $param = $param['id']; } return ! \Activitypub\is_blacklisted( $param ); @@ -138,13 +138,13 @@ class Inbox { 'required' => true, 'type' => array( 'object', 'string' ), 'validate_callback' => function( $param, $request, $key ) { - if ( ! is_string( $param ) ) { + if ( ! \is_string( $param ) ) { $param = $param['id']; } return ! \Activitypub\is_blacklisted( $param ); }, 'sanitize_callback' => function( $param, $request, $key ) { - if ( ! is_string( $param ) ) { + if ( ! \is_string( $param ) ) { $param = $param['id']; } return \esc_url_raw( $param ); diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 79926fc..841c6ba 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -19,7 +19,7 @@ class Server extends \WP_REST_Server { $content_type = $request->get_content_type(); // check for content-sub-types like 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' - if ( preg_match( '/application\/([a-zA-Z+_-]+\+)json/', $content_type['value'] ) ) { + if ( \preg_match( '/application\/([a-zA-Z+_-]+\+)json/', $content_type['value'] ) ) { $request->set_header( 'Content-Type', 'application/json' ); }