sanitize_text_field( $site_id ), 'secret' => $secret, 'domain' => sanitize_text_field( $domain ), 'connected_at' => time(), ), '', 'no' ); } public static function clear() { delete_option( self::OPTION ); delete_option( 'gralus_last_sync' ); } public static function app_url( $path ) { return rtrim( GRALUS_APP_URL, '/' ) . $path; } public static function sign( $secret, $ts, $method, $path, $body ) { $base = $ts . "\n" . strtoupper( $method ) . "\n" . $path . "\n" . hash( 'sha256', (string) $body ); return hash_hmac( 'sha256', $base, $secret ); } /** * Verify a request from Gralus. * * @param WP_REST_Request $request Request. * @return true|WP_Error */ public static function verify_request( $request ) { $c = self::get(); if ( ! $c ) { return new WP_Error( 'gralus_not_connected', 'This site is not connected to Gralus.', array( 'status' => 401 ) ); } $site = (string) $request->get_header( 'x_gralus_site' ); $ts = (string) $request->get_header( 'x_gralus_timestamp' ); $sig = (string) $request->get_header( 'x_gralus_signature' ); if ( ! hash_equals( $c['site_id'], $site ) || ! ctype_digit( $ts ) || '' === $sig ) { return new WP_Error( 'gralus_bad_signature', 'Signature check failed.', array( 'status' => 401 ) ); } if ( abs( time() - (int) $ts ) > self::MAX_SKEW ) { return new WP_Error( 'gralus_clock', 'Request is too old. Check the server clock.', array( 'status' => 401 ) ); } $expected = self::sign( $c['secret'], $ts, $request->get_method(), $request->get_route(), $request->get_body() ); if ( ! hash_equals( $expected, $sig ) ) { return new WP_Error( 'gralus_bad_signature', 'Signature check failed.', array( 'status' => 401 ) ); } return true; } /** * Swap a one-time code for this site's secret. * * @return true|WP_Error */ public static function connect_with_code( $code ) { $code = strtoupper( trim( preg_replace( '/\s+/', '', $code ) ) ); if ( ! preg_match( '/^GR-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/', $code ) ) { return new WP_Error( 'gralus_code', 'That does not look like a connection code. It starts with GR-.' ); } $res = wp_remote_post( self::app_url( '/api/wp/connect' ), array( 'timeout' => 20, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'code' => $code, 'siteUrl' => get_site_url(), 'homeUrl' => get_home_url(), 'pluginVersion' => GRALUS_VERSION, ) ), ) ); if ( is_wp_error( $res ) ) { return new WP_Error( 'gralus_network', 'Could not reach Gralus: ' . $res->get_error_message() ); } $json = json_decode( wp_remote_retrieve_body( $res ), true ); if ( 200 !== wp_remote_retrieve_response_code( $res ) || empty( $json['ok'] ) ) { $msg = isset( $json['message'] ) ? $json['message'] : 'Connection failed.'; return new WP_Error( 'gralus_rejected', $msg ); } self::save( $json['siteId'], $json['secret'], $json['domain'] ); Gralus_Site_Info::send(); return true; } /** * Signed POST from this site to Gralus. * * @return array|WP_Error */ public static function post_to_app( $path, $payload ) { $c = self::get(); if ( ! $c ) { return new WP_Error( 'gralus_not_connected', 'Not connected.' ); } $body = wp_json_encode( $payload ); $ts = (string) time(); $res = wp_remote_post( self::app_url( $path ), array( 'timeout' => 20, 'headers' => array( 'Content-Type' => 'application/json', 'X-Gralus-Site' => $c['site_id'], 'X-Gralus-Timestamp' => $ts, 'X-Gralus-Signature' => self::sign( $c['secret'], $ts, 'POST', $path, $body ), ), 'body' => $body, ) ); if ( is_wp_error( $res ) ) { return $res; } $json = json_decode( wp_remote_retrieve_body( $res ), true ); if ( 200 !== wp_remote_retrieve_response_code( $res ) ) { return new WP_Error( 'gralus_http', isset( $json['message'] ) ? $json['message'] : 'Request failed.' ); } return is_array( $json ) ? $json : array(); } }