<?php
declare(strict_types=1);

/**
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link          https://cakephp.org CakePHP(tm) Project
 * @since         3.0.0
 * @license       https://opensource.org/licenses/mit-license.php MIT License
 */
namespace Cake\Http\Client\Auth;

use Cake\Http\Client;
use Cake\Http\Client\Request;

/**
 * Digest authentication adapter for Cake\Http\Client
 *
 * Generally not directly constructed, but instead used by {@link \Cake\Http\Client}
 * when $options['auth']['type'] is 'digest'
 */
class Digest
{
    /**
     * Instance of Cake\Http\Client
     *
     * @var \Cake\Http\Client
     */
    protected $_client;

    /**
     * Constructor
     *
     * @param \Cake\Http\Client $client Http client object.
     * @param array|null $options Options list.
     */
    public function __construct(Client $client, ?array $options = null)
    {
        $this->_client = $client;
    }

    /**
     * Add Authorization header to the request.
     *
     * @param \Cake\Http\Client\Request $request The request object.
     * @param array<string, mixed> $credentials Authentication credentials.
     * @return \Cake\Http\Client\Request The updated request.
     * @see https://www.ietf.org/rfc/rfc2617.txt
     */
    public function authentication(Request $request, array $credentials): Request
    {
        if (!isset($credentials['username'], $credentials['password'])) {
            return $request;
        }
        if (!isset($credentials['realm'])) {
            $credentials = $this->_getServerInfo($request, $credentials);
        }
        if (!isset($credentials['realm'])) {
            return $request;
        }
        $value = $this->_generateHeader($request, $credentials);

        return $request->withHeader('Authorization', $value);
    }

    /**
     * Retrieve information about the authentication
     *
     * Will get the realm and other tokens by performing
     * another request without authentication to get authentication
     * challenge.
     *
     * @param \Cake\Http\Client\Request $request The request object.
     * @param array $credentials Authentication credentials.
     * @return array modified credentials.
     */
    protected function _getServerInfo(Request $request, array $credentials): array
    {
        $response = $this->_client->get(
            (string)$request->getUri(),
            [],
            ['auth' => ['type' => null]]
        );

        if (!$response->getHeader('WWW-Authenticate')) {
            return [];
        }
        preg_match_all(
            '@(\w+)=(?:(?:")([^"]+)"|([^\s,$]+))@',
            $response->getHeaderLine('WWW-Authenticate'),
            $matches,
            PREG_SET_ORDER
        );
        foreach ($matches as $match) {
            $credentials[$match[1]] = $match[2];
        }
        if (!empty($credentials['qop']) && empty($credentials['nc'])) {
            $credentials['nc'] = 1;
        }

        return $credentials;
    }

    /**
     * Generate the header Authorization
     *
     * @param \Cake\Http\Client\Request $request The request object.
     * @param array<string, mixed> $credentials Authentication credentials.
     * @return string
     */
    protected function _generateHeader(Request $request, array $credentials): string
    {
        $path = $request->getUri()->getPath();
        $a1 = md5($credentials['username'] . ':' . $credentials['realm'] . ':' . $credentials['password']);
        $a2 = md5($request->getMethod() . ':' . $path);
        $nc = '';

        if (empty($credentials['qop'])) {
            $response = md5($a1 . ':' . $credentials['nonce'] . ':' . $a2);
        } else {
            $credentials['cnonce'] = uniqid();
            $nc = sprintf('%08x', $credentials['nc']++);
            $response = md5(
                $a1 . ':' . $credentials['nonce'] . ':' . $nc . ':' . $credentials['cnonce'] . ':auth:' . $a2
            );
        }

        $authHeader = 'Digest ';
        $authHeader .= 'username="' . str_replace(['\\', '"'], ['\\\\', '\\"'], $credentials['username']) . '", ';
        $authHeader .= 'realm="' . $credentials['realm'] . '", ';
        $authHeader .= 'nonce="' . $credentials['nonce'] . '", ';
        $authHeader .= 'uri="' . $path . '", ';
        $authHeader .= 'response="' . $response . '"';
        if (!empty($credentials['opaque'])) {
            $authHeader .= ', opaque="' . $credentials['opaque'] . '"';
        }
        if (!empty($credentials['qop'])) {
            $authHeader .= ', qop="auth", nc=' . $nc . ', cnonce="' . $credentials['cnonce'] . '"';
        }

        return $authHeader;
    }
}
