<?php

namespace WPLEClient;

use WPLEClient\Exceptions\LEConnectorException;

/**
 * LetsEncrypt Connector class, containing the functions necessary to sign with JSON Web Key and Key ID, and perform GET, POST and HEAD requests.
 *
 * PHP version 5.2.0
 *
 * MIT License
 *
 * Copyright (c) 2018 Youri van Weegberg
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * @author     Youri van Weegberg <youri@yourivw.nl>
 * @copyright  2018 Youri van Weegberg
 * @license    https://opensource.org/licenses/mit-license.php  MIT License
 * @link       https://github.com/yourivw/LEClient
 * @since      Class available since Release 1.0.0
 */
class LEConnector
{
  public $baseURL;
  public $accountKeys;

  private $nonce;

  public $keyChange;
  public $newAccount;
  public $newNonce;
  public $newOrder;
  public $revokeCert;

  public $accountURL;
  public $accountDeactivated = false;

  private $log;

  /**
   * Initiates the LetsEncrypt Connector class.
   *
   * @param int 		$log			The level of logging. Defaults to no logging. LOG_OFF, LOG_STATUS, LOG_DEBUG accepted.
   * @param string	$baseURL 		The LetsEncrypt server URL to make requests to.
   * @param array		$accountKeys 	Array containing location of account keys files.
   */
  public function __construct($log, $baseURL, $accountKeys)
  {

    foreach ($accountKeys as $id => $pky) {
      $accountKeys[$id] = str_ireplace(ABSPATH . ABSPATH, ABSPATH, $pky);
    }
    $this->baseURL = $baseURL;
    $this->accountKeys = $accountKeys;
    $this->log = $log;
    $this->getLEDirectory();
    $this->getNewNonce();
  }

  /**
   * Requests the LetsEncrypt Directory and stores the necessary URLs in this LetsEncrypt Connector instance.
   */
  private function getLEDirectory()
  {
    $req = $this->get('/directory');
    $this->keyChange = $req['body']['keyChange'];
    $this->newAccount = $req['body']['newAccount'];
    $this->newNonce = $req['body']['newNonce'];
    $this->newOrder = $req['body']['newOrder'];
    $this->revokeCert = $req['body']['revokeCert'];
  }

  /**
   * Requests a new nonce from the LetsEncrypt server and stores it in this LetsEncrypt Connector instance.
   */
  private function getNewNonce()
  {
    if ($this->head($this->newNonce)['status'] !== 200) throw LEConnectorException::NoNewNonceException();
  }

  /**
   * Makes a Curl request.
   *
   * @param string	$method	The HTTP method to use. Accepting GET, POST and HEAD requests.
   * @param string 	$URL 	The URL or partial URL to make the request to. If it is partial, the baseURL will be prepended.
   * @param object 	$data  	The body to attach to a POST request. Expected as a JSON encoded string.
   *
   * @return array 	Returns an array with the keys 'request', 'header', 'status' and 'body'.
   */
  private function request($method, $URL, $data = null)
  {
    if ($this->accountDeactivated) throw LEConnectorException::AccountDeactivatedException();

    $headers = array('Accept: application/json', 'Content-Type: application/jose+json');
    $requestURL = preg_match('~^http~', $URL) ? $URL : $this->baseURL . $URL;
    $handle = curl_init();
    curl_setopt($handle, CURLOPT_URL, $requestURL);
    curl_setopt($handle, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($handle, CURLOPT_HEADER, true);
    curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, false);

    switch ($method) {
      case 'GET':
        break;
      case 'POST':
        curl_setopt($handle, CURLOPT_POST, true);
        curl_setopt($handle, CURLOPT_POSTFIELDS, $data);
        break;
      case 'HEAD':
        curl_setopt($handle, CURLOPT_CUSTOMREQUEST, 'HEAD');
        curl_setopt($handle, CURLOPT_NOBODY, true);
        break;
      default:
        throw LEConnectorException::MethodNotSupportedException($method);
        break;
    }
    $response = curl_exec($handle);

    if (curl_errno($handle)) {
      throw LEConnectorException::CurlErrorException(curl_error($handle));
    }

    $headerSize = curl_getinfo($handle, CURLINFO_HEADER_SIZE);
    $statusCode = curl_getinfo($handle, CURLINFO_HTTP_CODE);

    $header = substr($response, 0, $headerSize);
    $body = substr($response, $headerSize);
    $jsonbody = json_decode($body, true);
    $jsonresponse = array(
      'request' => $method . ' ' . $requestURL,
      'header' => $header,
      'status' => $statusCode,
      'body' => $jsonbody === null ? $body : $jsonbody,
    );
    if ($this->log instanceof \Psr\Log\LoggerInterface) {
      $this->log->debug($method . ' response received', $jsonresponse);
    } elseif ($this->log >= LEClient::LOG_DEBUG) LEFunctions::log($jsonresponse);

    if (preg_match('~Replay\-Nonce: (\S+)~i', $header, $matches)) {
      $this->nonce = trim($matches[1]);
    } else {
      if ($method == 'POST') $this->getNewNonce(); // Not expecting a new nonce with GET and HEAD requests.
    }

    if ((($method == 'POST' or $method == 'GET') and $statusCode !== 200 and $statusCode !== 201) or ($method == 'HEAD' and $statusCode !== 200)
    ) {
      throw LEConnectorException::InvalidResponseException($jsonresponse);
    }

    return $jsonresponse;
  }

  /**
   * Makes a GET request.
   *
   * @param string	$url 	The URL or partial URL to make the request to. If it is partial, the baseURL will be prepended.
   *
   * @return array 	Returns an array with the keys 'request', 'header', 'status' and 'body'.
   */
  public function get($url)
  {
    return $this->request('GET', $url);
  }

  /**
   * Makes a POST request.
   *
   * @param string 	$url	The URL or partial URL to make the request to. If it is partial, the baseURL will be prepended.
   * @param object 	$data	The body to attach to a POST request. Expected as a json string.
   *
   * @return array 	Returns an array with the keys 'request', 'header', 'status' and 'body'.
   */
  public function post($url, $data = null)
  {
    return $this->request('POST', $url, $data);
  }

  /**
   * Makes a HEAD request.
   *
   * @param string 	$url	The URL or partial URL to make the request to. If it is partial, the baseURL will be prepended.
   *
   * @return array	Returns an array with the keys 'request', 'header', 'status' and 'body'.
   */
  public function head($url)
  {
    return $this->request('HEAD', $url);
  }

  /**
   * Generates a JSON Web Key signature to attach to the request.
   *
   * @param array 	$payload		The payload to add to the signature.
   * @param string	$url 			The URL to use in the signature.
   * @param string 	$privateKeyFile The private key to sign the request with. Defaults to 'private.pem'. Defaults to accountKeys[private_key].
   *
   * @return string	Returns a JSON encoded string containing the signature.
   */
  public function signRequestJWK($payload, $url, $privateKeyFile = '')
  {
    if ($privateKeyFile == '') $privateKeyFile = $this->accountKeys['private_key'];
    $privateKey = openssl_pkey_get_private(file_get_contents($privateKeyFile));
    $details = openssl_pkey_get_details($privateKey);

    $protected = array(
      "alg" => "RS256",
      "jwk" => array(
        "kty" => "RSA",
        "n" => LEFunctions::Base64UrlSafeEncode($details["rsa"]["n"]),
        "e" => LEFunctions::Base64UrlSafeEncode($details["rsa"]["e"]),
      ),
      "nonce" => $this->nonce,
      "url" => $url
    );

    $payload64 = LEFunctions::Base64UrlSafeEncode(str_replace('\\/', '/', is_array($payload) ? json_encode($payload) : $payload));
    $protected64 = LEFunctions::Base64UrlSafeEncode(json_encode($protected));

    openssl_sign($protected64 . '.' . $payload64, $signed, $privateKey, "SHA256");
    $signed64 = LEFunctions::Base64UrlSafeEncode($signed);

    $data = array(
      'protected' => $protected64,
      'payload' => $payload64,
      'signature' => $signed64
    );

    return json_encode($data);
  }

  /**
   * Generates a Key ID signature to attach to the request.
   *
   * @param array 	$payload		The payload to add to the signature.
   * @param string	$kid			The Key ID to use in the signature.
   * @param string	$url 			The URL to use in the signature.
   * @param string 	$privateKeyFile The private key to sign the request with. Defaults to 'private.pem'. Defaults to accountKeys[private_key].
   *
   * @return string	Returns a JSON encoded string containing the signature.
   */
  public function signRequestKid($payload, $kid, $url, $privateKeyFile = '')
  {
    if ($privateKeyFile == '') $privateKeyFile = $this->accountKeys['private_key'];
    $privateKey = openssl_pkey_get_private(file_get_contents($privateKeyFile));
    $details = openssl_pkey_get_details($privateKey);

    $protected = array(
      "alg" => "RS256",
      "kid" => $kid,
      "nonce" => $this->nonce,
      "url" => $url
    );

    $payload64 = LEFunctions::Base64UrlSafeEncode(str_replace('\\/', '/', is_array($payload) ? json_encode($payload) : $payload));
    $protected64 = LEFunctions::Base64UrlSafeEncode(json_encode($protected));

    openssl_sign($protected64 . '.' . $payload64, $signed, $privateKey, "SHA256");
    $signed64 = LEFunctions::Base64UrlSafeEncode($signed);

    $data = array(
      'protected' => $protected64,
      'payload' => $payload64,
      'signature' => $signed64
    );

    return json_encode($data);
  }
}
