<?php
/**
* SeekQuarry/Yioop --
* Open Source Pure PHP Search Engine, Crawler, and Indexer
*
* Copyright (C) 2009 - 2026 Chris Pollett chris@pollett.org
*
* LICENSE:
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*
* END LICENSE
*
* @author Chris Pollett chris@pollett.org
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009 - 2026
* @filesource
*/
namespace seekquarry\yioop\library\certificates;
use seekquarry\yioop\configs as C;
/**
* AcmeChallengeStore keeps, on disk, what answer belongs to each
* challenge token a certificate authority has set. Automatic Certificate
* Management Environment, which is what ACME stands for, proves a site
* owns its name by asking the site to serve a given answer at a given
* address over plain HTTP; that exchange is its http-01 challenge. During
* certificate issuance the ACME subsystem
* calls put() for each pending challenge; the secure launcher's
* port-80 acme-challenge route calls get() when the certificate
* authority fetches /.well-known/acme-challenge/<token>, and the
* subsystem calls clear() once the challenge is validated.
*
* Storing the mapping on disk rather than in memory lets the
* issuing process (which may be a media job or a CLI command) and
* the serving process (the running WebSite) be different processes:
* they share only the directory.
*
* The token arrives from an external HTTP request, so every method
* validates it against the base64url character set ACME tokens use
* before forming a path. A token containing anything else -- in
* particular a slash, backslash, or dot -- is rejected, so no
* request can escape the challenge directory.
*
* @author Chris Pollett chris@pollett.org
*/
class AcmeChallengeStore
{
/**
* Absolute path of the directory holding one file per pending
* challenge token.
* @var string
*/
private $dir;
/**
* __construct names the folder the answers are kept in.
*
* @param string $dir folder for token files; where none is given,
* the folder named by ACME_CHALLENGE_DIR is used
*/
public function __construct($dir = null)
{
$this->dir = ($dir === null) ? C\ACME_CHALLENGE_DIR : $dir;
}
/**
* ensureDir makes sure the challenge folder is there, creating it (and any
* missing parents) on first use.
*
* @return bool true if the directory exists or was created
*/
private function ensureDir()
{
if (is_dir($this->dir)) {
return true;
}
return @mkdir($this->dir, 0700, true) || is_dir($this->dir);
}
/**
* tokenPath gives the whole path of the file holding a token's
* answer, or null if the
* token is invalid.
*
* @param string $token challenge token
* @return string|null path, or null when the token is rejected
*/
private function tokenPath($token)
{
if (!(is_string($token) && $token !== "" &&
preg_match('#^[A-Za-z0-9_-]+$#', $token) === 1)) {
return null;
}
return $this->dir . "/" . $token;
}
/**
* put writes down the answer belonging to a challenge token so the
* acme-challenge route can serve it.
*
* @param string $token challenge token
* @param string $key_authorization the token's key
* authorization (token.thumbprint)
* @return bool true on success, false on an invalid token or a
* write failure
*/
public function put($token, $key_authorization)
{
$path = $this->tokenPath($token);
if ($path === null || !$this->ensureDir()) {
return false;
}
return @file_put_contents($path, $key_authorization) !== false;
}
/**
* get gives back the answer written down for a token, or null if
* the token is invalid or no entry exists.
*
* @param string $token challenge token
* @return string|null the key authorization, or null
*/
public function get($token)
{
$path = $this->tokenPath($token);
if ($path === null || !is_file($path)) {
return null;
}
$value = @file_get_contents($path);
return ($value === false) ? null : $value;
}
/**
* clear takes a token's entry away once its challenge is validated.
* Returns true when no entry remains, whether it was deleted or
* was already absent.
*
* @param string $token challenge token
* @return bool true if no entry remains
*/
public function clear($token)
{
$path = $this->tokenPath($token);
if ($path === null) {
return false;
}
if (!is_file($path)) {
return true;
}
return @unlink($path);
}
}