You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

558 lines
13 KiB

<?php
class wfCentralAPIRequest {
/**
* @var string
*/
private $endpoint;
/**
* @var string
*/
private $method;
/**
* @var null
*/
private $token;
/**
* @var array
*/
private $body;
/**
* @var array
*/
private $args;
/**
* @param string $endpoint
* @param string $method
* @param string|null $token
* @param array $body
* @param array $args
*/
public function __construct($endpoint, $method = 'GET', $token = null, $body = array(), $args = array()) {
$this->endpoint = $endpoint;
$this->method = $method;
$this->token = $token;
$this->body = $body;
$this->args = $args;
}
public function execute() {
$args = array(
'timeout' => 10,
);
$args = wp_parse_args($this->getArgs(), $args);
$args['method'] = $this->getMethod();
if (empty($args['headers'])) {
$args['headers'] = array();
}
$token = $this->getToken();
if ($token) {
$args['headers']['Authorization'] = 'Bearer ' . $token;
}
if ($this->getBody()) {
$args['headers']['Content-Type'] = 'application/json';
$args['body'] = json_encode($this->getBody());
}
$http = _wp_http_get_object();
$response = $http->request(WORDFENCE_CENTRAL_API_URL_SEC . $this->getEndpoint(), $args);
return new wfCentralAPIResponse($response);
}
/**
* @return string
*/
public function getEndpoint() {
return $this->endpoint;
}
/**
* @param string $endpoint
*/
public function setEndpoint($endpoint) {
$this->endpoint = $endpoint;
}
/**
* @return string
*/
public function getMethod() {
return $this->method;
}
/**
* @param string $method
*/
public function setMethod($method) {
$this->method = $method;
}
/**
* @return null
*/
public function getToken() {
return $this->token;
}
/**
* @param null $token
*/
public function setToken($token) {
$this->token = $token;
}
/**
* @return array
*/
public function getBody() {
return $this->body;
}
/**
* @param array $body
*/
public function setBody($body) {
$this->body = $body;
}
/**
* @return array
*/
public function getArgs() {
return $this->args;
}
/**
* @param array $args
*/
public function setArgs($args) {
$this->args = $args;
}
}
class wfCentralAPIResponse {
public static function parseErrorJSON($json) {
$data = json_decode($json, true);
if (is_array($data) && array_key_exists('message', $data)) {
return $data['message'];
}
return $json;
}
/**
* @var array|null
*/
private $response;
/**
* @param array $response
*/
public function __construct($response = null) {
$this->response = $response;
}
public function getStatusCode() {
return wp_remote_retrieve_response_code($this->getResponse());
}
public function getBody() {
return wp_remote_retrieve_body($this->getResponse());
}
public function getJSONBody() {
return json_decode($this->getBody(), true);
}
public function isError() {
if (is_wp_error($this->getResponse())) {
return true;
}
$statusCode = $this->getStatusCode();
return !($statusCode >= 200 && $statusCode < 300);
}
public function returnErrorArray() {
return array(
'err' => 1,
'errorMsg' => sprintf(__('HTTP %d received from Wordfence Central: %s', 'wordfence'),
$this->getStatusCode(), $this->parseErrorJSON($this->getBody())),
);
}
/**
* @return array|null
*/
public function getResponse() {
return $this->response;
}
/**
* @param array|null $response
*/
public function setResponse($response) {
$this->response = $response;
}
}
class wfCentralAuthenticatedAPIRequest extends wfCentralAPIRequest {
private $retries = 3;
/**
* @param string $endpoint
* @param string $method
* @param array $body
* @param array $args
*/
public function __construct($endpoint, $method = 'GET', $body = array(), $args = array()) {
parent::__construct($endpoint, $method, null, $body, $args);
}
/**
* @return mixed|null
* @throws wfCentralAPIException
*/
public function getToken() {
$token = parent::getToken();
if ($token) {
return $token;
}
$token = get_transient('wordfenceCentralJWT' . wfConfig::get('wordfenceCentralSiteID'));
if ($token) {
return $token;
}
for ($i = 0; $i < $this->retries; $i++) {
try {
$token = $this->fetchToken();
break;
} catch (wfCentralAPIException $e) {
continue;
}
}
if (empty($token)) {
if (isset($e)) {
throw $e;
} else {
throw new wfCentralAPIException(__('Unable to authenticate with Wordfence Central.', 'wordfence'));
}
}
$tokenContents = wfJWT::extractTokenContents($token);
if (!empty($tokenContents['body']['exp'])) {
set_transient('wordfenceCentralJWT' . wfConfig::get('wordfenceCentralSiteID'), $token, $tokenContents['body']['exp'] - time());
}
return $token;
}
public function fetchToken() {
require_once(WORDFENCE_PATH . '/crypto/vendor/paragonie/sodium_compat/autoload-fast.php');
$defaultArgs = array(
'timeout' => 6,
);
$siteID = wfConfig::get('wordfenceCentralSiteID');
if (!$siteID) {
throw new wfCentralAPIException(__('Wordfence Central site ID has not been created yet.', 'wordfence'));
}
$secretKey = wfConfig::get('wordfenceCentralSecretKey');
if (!$secretKey) {
throw new wfCentralAPIException(__('Wordfence Central secret key has not been created yet.', 'wordfence'));
}
// Pull down nonce.
$request = new wfCentralAPIRequest(sprintf('/site/%s/login', $siteID), 'GET', null, array(), $defaultArgs);
$nonceResponse = $request->execute();
if ($nonceResponse->isError()) {
$errorArray = $nonceResponse->returnErrorArray();
throw new wfCentralAPIException($errorArray['errorMsg']);
}
$body = $nonceResponse->getJSONBody();
if (!is_array($body) || !isset($body['nonce'])) {
throw new wfCentralAPIException(__('Invalid response received from Wordfence Central when fetching nonce.', 'wordfence'));
}
$nonce = $body['nonce'];
// Sign nonce to pull down JWT.
$data = $nonce . '|' . $siteID;
$signature = ParagonIE_Sodium_Compat::crypto_sign_detached($data, $secretKey);
$request = new wfCentralAPIRequest(sprintf('/site/%s/login', $siteID), 'POST', null, array(
'data' => $data,
'signature' => ParagonIE_Sodium_Compat::bin2hex($signature),
), $defaultArgs);
$authResponse = $request->execute();
if ($authResponse->isError()) {
$errorArray = $authResponse->returnErrorArray();
throw new wfCentralAPIException($errorArray['errorMsg']);
}
$body = $authResponse->getJSONBody();
if (!is_array($body)) {
throw new wfCentralAPIException(__('Invalid response received from Wordfence Central when fetching token.', 'wordfence'));
}
if (!isset($body['jwt'])) { // Possible authentication error.
throw new wfCentralAPIException(__('Unable to authenticate with Wordfence Central.', 'wordfence'));
}
return $body['jwt'];
}
}
class wfCentralAPIException extends Exception {
}
class wfCentral {
/**
* @return bool
*/
public static function isSupported() {
return function_exists('register_rest_route') && version_compare(phpversion(), '5.3', '>=');
}
/**
* @return bool
*/
public static function isConnected() {
return self::isSupported() && ((bool) wfConfig::get('wordfenceCentralConnected', false));
}
/**
* @return bool
*/
public static function isPartialConnection() {
return !wfConfig::get('wordfenceCentralConnected') && wfConfig::get('wordfenceCentralSiteID');
}
/**
* @param array $issue
* @return bool|wfCentralAPIResponse
*/
public static function sendIssue($issue) {
return self::sendIssues(array($issue));
}
/**
* @param $issues
* @return bool|wfCentralAPIResponse
*/
public static function sendIssues($issues) {
$data = array();
foreach ($issues as $issue) {
$issueData = array(
'type' => 'issue',
'attributes' => $issue,
);
if (array_key_exists('id', $issueData)) {
$issueData['id'] = $issue['id'];
}
$data[] = $issueData;
}
$siteID = wfConfig::get('wordfenceCentralSiteID');
$request = new wfCentralAuthenticatedAPIRequest('/site/' . $siteID . '/issues', 'POST', array(
'data' => $data,
));
try {
$response = $request->execute();
return $response;
} catch (wfCentralAPIException $e) {
error_log($e);
}
return false;
}
/**
* @param int $issueID
* @return bool|wfCentralAPIResponse
*/
public static function deleteIssue($issueID) {
return self::deleteIssues(array($issueID));
}
/**
* @param $issues
* @return bool|wfCentralAPIResponse
*/
public static function deleteIssues($issues) {
$siteID = wfConfig::get('wordfenceCentralSiteID');
$request = new wfCentralAuthenticatedAPIRequest('/site/' . $siteID . '/issues', 'DELETE', array(
'data' => array(
'type' => 'issue-list',
'attributes' => array(
'ids' => $issues,
)
),
));
try {
$response = $request->execute();
return $response;
} catch (wfCentralAPIException $e) {
error_log($e);
}
return false;
}
/**
* @return bool|wfCentralAPIResponse
*/
public static function deleteNewIssues() {
$siteID = wfConfig::get('wordfenceCentralSiteID');
$request = new wfCentralAuthenticatedAPIRequest('/site/' . $siteID . '/issues', 'DELETE', array(
'data' => array(
'type' => 'issue-list',
'attributes' => array(
'status' => 'new',
)
),
));
try {
$response = $request->execute();
return $response;
} catch (wfCentralAPIException $e) {
error_log($e);
}
return false;
}
/**
* @param array $types Array of issue types to delete
* @param string $status Issue status to delete
* @return bool|wfCentralAPIResponse
*/
public static function deleteIssueTypes($types, $status = 'new') {
$siteID = wfConfig::get('wordfenceCentralSiteID');
$request = new wfCentralAuthenticatedAPIRequest('/site/' . $siteID . '/issues', 'DELETE', array(
'data' => array(
'type' => 'issue-list',
'attributes' => array(
'types' => $types,
'status' => $status,
)
),
));
try {
$response = $request->execute();
return $response;
} catch (wfCentralAPIException $e) {
error_log($e);
}
return false;
}
public static function requestConfigurationSync() {
if (! wfCentral::isConnected() || !self::$syncConfig) {
return;
}
$endpoint = '/site/'.wfConfig::get('wordfenceCentralSiteID').'/config';
$args = array('timeout' => 0.01, 'blocking' => false);
$request = new wfCentralAuthenticatedAPIRequest($endpoint, 'POST', array(), $args);
try {
$request->execute();
} catch (Exception $e) {
// We can safely ignore an error here for now.
}
}
protected static $syncConfig = true;
public static function preventConfigurationSync() {
self::$syncConfig = false;
}
/**
* @param $scan
* @param $running
* @return bool|wfCentralAPIResponse
*/
public static function updateScanStatus($scan = null) {
if ($scan === null) {
$scan = wfConfig::get_ser('scanStageStatuses');
if (!is_array($scan)) {
$scan = array();
}
}
$siteID = wfConfig::get('wordfenceCentralSiteID');
$running = wfScanner::shared()->isRunning();
$request = new wfCentralAuthenticatedAPIRequest('/site/' . $siteID . '/scan', 'PATCH', array(
'data' => array(
'type' => 'scan',
'attributes' => array(
'running' => $running,
'scan' => $scan,
'scan-summary' => wfConfig::get('wf_summaryItems'),
),
),
));
try {
$response = $request->execute();
return $response;
} catch (wfCentralAPIException $e) {
error_log($e);
}
return false;
}
/**
* @param string $event
* @param array $data
* @param callable|null $alertCallback
*/
public static function sendSecurityEvent($event, $data = array(), $alertCallback = null) {
$alerted = false;
if (!self::pluginAlertingDisabled() && is_callable($alertCallback)) {
call_user_func($alertCallback);
$alerted = true;
}
$siteID = wfConfig::get('wordfenceCentralSiteID');
$request = new wfCentralAuthenticatedAPIRequest('/site/' . $siteID . '/security-events', 'POST', array(
'data' => array(
array(
'type' => 'security-event',
'attributes' => array(
'type' => $event,
'data' => $data,
'event_time' => microtime(true),
),
),
),
));
try {
// Attempt to send the security event to Central.
$response = $request->execute();
} catch (wfCentralAPIException $e) {
// If we didn't alert previously, notify the user now in the event Central is down.
if (!$alerted && is_callable($alertCallback)) {
call_user_func($alertCallback);
}
}
}
/**
* @param $event
* @param array $data
* @param callable|null $alertCallback
*/
public static function sendAlertCallback($event, $data = array(), $alertCallback = null) {
if (is_callable($alertCallback)) {
call_user_func($alertCallback);
}
}
public static function pluginAlertingDisabled() {
if (!self::isConnected()) {
return false;
}
return wfConfig::get('wordfenceCentralPluginAlertingDisabled', false);
}
}