server = $server; } public function register_rest(): void { register_rest_route('relay/v1', '/remote', [ 'methods' => 'POST', 'callback' => [$this, 'rest'], 'permission_callback' => [$this, 'permission'], // the request's signature, checked before the handler runs ]); add_filter('rest_pre_serve_request', [$this, 'before_serving'], 10, 3); } /** * Only requests signed with one of this site's access keys reach the handler. A refusal carries SiteRelay's own * answer in its data (Transport), so the other site still reads why. * * @return true|\WP_Error */ public function permission(\WP_REST_Request $request) { $refusal = $this->server->authenticate(self::headers($request), (string) $request->get_body()); if ($refusal === null) { return true; } return new \WP_Error('relay_refused', __('This request is not signed with one of this site\'s access keys.', 'siterelay'), ['status' => $refusal['status']] + Transport::wrap($refusal['body'])); } /** The headers a signed request carries (Server::handle()); nothing else of the request is read. */ private const HEADERS = ['x-relay-key', 'x-relay-op', 'x-relay-job', 'x-relay-timestamp', 'x-relay-nonce', 'x-relay-signature']; /** @return array lower-case names with dashes */ private static function headers(\WP_REST_Request $request): array { $headers = []; foreach (self::HEADERS as $name) { $value = $request->get_header($name); if ($value !== null) { $headers[$name] = sanitize_text_field($value); } } return $headers; } public function rest(\WP_REST_Request $request): \WP_REST_Response { $out = $this->server->handle(self::headers($request), (string) $request->get_body()); $response = new \WP_REST_Response(Transport::wrap($out['body']), $out['status']); if ($out['signature'] !== null) { $response->header('X-Relay-Signature', $out['signature']); } return $response; } /** * Before WordPress outputs the answer to a signed request: whatever another plugin printed earlier (a notice, * whitespace) goes, so the JSON reads. * * @param bool $served whether the response was already sent * @param mixed $result the response * @param mixed $request the request * @return bool */ public function before_serving($served, $result, $request) { if (!$served && $request instanceof \WP_REST_Request && $request->get_route() === '/relay/v1/remote') { self::discard_output(); } return $served; } public function ajax(): void { $headers = []; foreach (self::HEADERS as $name) { $key = 'HTTP_' . strtoupper(str_replace('-', '_', $name)); if (isset($_SERVER[$key])) { $headers[$name] = sanitize_text_field(wp_unslash($_SERVER[$key])); } } $out = $this->server->handle($headers, (string) file_get_contents('php://input')); self::discard_output(); if ($out['signature'] !== null) { header('X-Relay-Signature: ' . $out['signature']); } wp_send_json(Transport::wrap($out['body']), $out['status']); } /** Anything another plugin printed earlier (notices, whitespace) would corrupt the signed body. */ private static function discard_output(): void { while (ob_get_level() > 0) { ob_end_clean(); } } }