This commit is contained in:
以诺书 2024-08-05 22:57:28 +08:00
commit 0efda6c02a
1779 changed files with 171774 additions and 0 deletions

1
404.html Normal file
View File

@ -0,0 +1 @@
别急,还没弄好

1
acme Submodule

@ -0,0 +1 @@
Subproject commit 452d755719e53231601212b60ebb2e0855a857c3

0
acme.sh Normal file
View File

1
cacme/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
./keys/*

21
cacme/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 walkor<walkor@workerman.net> and contributors (see https://github.com/walkor/webman/contributors)
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.

4
cacme/README.md Normal file
View File

@ -0,0 +1,4 @@
# PHP ACME Cli in Webman
run:
php webman acme

133
cacme/app/command/Acme.php Normal file
View File

@ -0,0 +1,133 @@
<?php
namespace app\command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use AcmePhp\Core\Http\Base64SafeEncoder;
use AcmePhp\Core\Http\SecureHttpClientFactory;
use AcmePhp\Core\Http\ServerErrorHandler;
use AcmePhp\Ssl\KeyPair;
use AcmePhp\Ssl\PrivateKey;
use AcmePhp\Ssl\PublicKey;
use AcmePhp\Ssl\Parser\KeyParser;
use AcmePhp\Ssl\Signer\DataSigner;
use GuzzleHttp\Client as GuzzleHttpClient;
use AcmePhp\Ssl\DistinguishedName;
use AcmePhp\Ssl\CertificateRequest;
use AcmePhp\Ssl\Generator\KeyPairGenerator;
use AcmePhp\Core\Challenge\Dns\DnsDataExtractor;
use AcmePhp\Core\AcmeClient;
class Acme extends Command
{
protected static $defaultName = 'acme';
protected static $defaultDescription = 'acme';
/**
* @return void
*/
protected function configure()
{
$this->addArgument('name', InputArgument::OPTIONAL, 'Name description');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$EMAIL='youremail@yourdomain.com';
$DOMAIN='yourdomain.com';
#$CA='https://acme-v02.api.letsencrypt.org/directory';
$CA='https://acme-staging-v02.api.letsencrypt.org/directory';
$secureHttpClientFactory = new SecureHttpClientFactory(
new GuzzleHttpClient(),
new Base64SafeEncoder(),
new KeyParser(),
new DataSigner(),
new ServerErrorHandler()
);
$publicKeyPath = base_path().'/keys/account.pub.pem';
$privateKeyPath = base_path().'/keys/account.pem';
if (!file_exists($privateKeyPath)) {
$keyPairGenerator = new KeyPairGenerator();
$keyPair = $keyPairGenerator->generateKeyPair();
file_put_contents($publicKeyPath, $keyPair->getPublicKey()->getPEM());
file_put_contents($privateKeyPath, $keyPair->getPrivateKey()->getPEM());
} else {
$publicKey = new PublicKey(file_get_contents($publicKeyPath));
$privateKey = new PrivateKey(file_get_contents($privateKeyPath));
$keyPair = new KeyPair($publicKey, $privateKey);
}
$secureHttpClient = $secureHttpClientFactory->createSecureHttpClient($keyPair);
#$acmeClient = new AcmeClient($secureHttpClient, 'https://acme-v02.api.letsencrypt.org/directory');
$acmeClient = new AcmeClient($secureHttpClient, $CA);
$acmeClient->registerAccount($EMAIL,null);
$authorizationChallenges = $acmeClient->requestAuthorization($DOMAIN);
foreach($authorizationChallenges as $id=>$cha){
$cha->id=$id;
$dde=new DnsDataExtractor;
$cha->value=$dde->getRecordValue($cha);
print_r($cha);
}
#print_r($authorizationChallenges);
#print_r($dde->getRecordValue($authorizationChallenges[1]));
#$output->writeln($authorizationChallenges);
$helper = $this->getHelper('question');
$question = new ChoiceQuestion(
'选择对应的ID',
array('0', '1', '2'),
0
);
$way = $helper->ask($input, $output, $question);
$acmeClient->challengeAuthorization($authorizationChallenges[$way]);
$dn = new DistinguishedName($DOMAIN);
$keyPairGenerator = new KeyPairGenerator();
// Make a new key pair. We'll keep the private key as our cert key
$domainKeyPair = $keyPairGenerator->generateKeyPair();
// This is the private key
var_dump($domainKeyPair->getPrivateKey()->getPem());
// Generate CSR
$csr = new CertificateRequest($dn, $domainKeyPair);
$certificateResponse = $acmeClient->requestCertificate($DOMAIN, $csr);
// This is the certificate (public key)
var_dump($certificateResponse->getCertificate()->getPem());
// For Let's Encrypt, you will need the intermediate too
var_dump($certificateResponse->getCertificate()->getIssuerCertificate()->getPEM());
return self::SUCCESS;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace app\controller;
use support\Request;
use Respect\Validation\Validator as v;
class Apply
{
public function check(Request $request)
{
$email=$request->input('email',null);
$domain=$request->input('domain',null);
$cert=$request->input('cert',null);
if(!$email||!$domain||!$cert){
return json(['code'=>404,'msg'=>'缺少参数,请刷新重试']);
}
if(!in_array($cert,array('R3','Laysense','FWNET'))){
return json(['code'=>404,'msg'=>'证书类型错误或不存在']);
}
if(in_array($cert,array('Laysense','FWNET'))){
return json(['code'=>403,'msg'=>'当前证书类型'.$cert.'暂时无法颁发']);
}
if(!v::domain($innerDomain)->validate($domain))
return view('index');
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace app\controller;
use support\Request;
class IndexController
{
public function index(Request $request)
{
return view('index');
}
}

4
cacme/app/functions.php Normal file
View File

@ -0,0 +1,4 @@
<?php
/**
* Here is your custom functions.
*/

View File

@ -0,0 +1,42 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
/**
* Class StaticFile
* @package app\middleware
*/
class StaticFile implements MiddlewareInterface
{
public function process(Request $request, callable $next): Response
{
// Access to files beginning with. Is prohibited
if (strpos($request->path(), '/.') !== false) {
return response('<h1>403 forbidden</h1>', 403);
}
/** @var Response $response */
$response = $next($request);
// Add cross domain HTTP header
/*$response->withHeaders([
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Credentials' => 'true',
]);*/
return $response;
}
}

29
cacme/app/model/Test.php Normal file
View File

@ -0,0 +1,29 @@
<?php
namespace app\model;
use support\Model;
class Test extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'test';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
}

183
cacme/app/view/index.html Normal file
View File

@ -0,0 +1,183 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<meta content="width=device-width, initial-scale=1" name="viewport">
<meta name="generator" content="pandoc" />
<title>Center ACME Auto SSL</title>
<style type="text/css">code{white-space: pre;}</style>
<link rel="stylesheet" href="normal.css" type="text/css" />
<link rel="stylesheet" href="sakura.css" type="text/css" />
<style>
body {
height: 100%;
}
.wrapper{
position:relative;
box-sizing:border-box;
min-height:100%;
padding-bottom:35px;
}
.footer{
position:absolute;
bottom:0;
left:0;
right:0;
height:35px;
}
</style>
</head>
<body>
<div class="wrapper">
<h1 id="sakura-a-minimal-classless-css-framework-theme.">
Center ACME Auto SSL(CAAS) by Laysense
</h1>
<p>[由 <a href="https://lab.laysense.cn/">来笙实验室</a> 出品]</p>
<blockquote>
<p>CAAS将打破你对ssl和acme的认识现在只需要设置一次302重定向就可以直接申请到证书辣</p>
</blockquote>
<button id="back" onclick="skipto('step1')">🔃重设</button>
<div id="step1" style="display: none;">
<center>
<h3>Step1:输入你需要申请SSL的域名</h3>
<input id="domain" type="url" /><br />
<h3>以及你的邮箱</h3>
<input id="email" type="email" />
<button onclick="skipto('step2')">👉下一步</button>
</div>
<div id="step2" style="display: none;">
<center>
<h3>Step2:选择一种证书类型</h3>
<ul>
<li>
<h5>正式使用、公共网站:</h5>
<button onclick="choose('R3')" style="padding:8px;">📃R3(3个月)</button><br>
(即Let's Encrypt)
</li><hr />
<li>
<h5>实验用途、私有网络:</h5>
<button onclick="skipto('step99')" style="background: linear-gradient(135deg, #c850c0, #4158d0);padding:15px;">💎来笙豪华娱乐证书(10年)</button>
</li>
<li>
<h5>5050net、fwnet内部使用</h5>
<button onclick="skipto('step999')">🌐FWNET证书</button>
</li>
</ul>
</center>
</div>
<div id="step3" style="display: none;">
<div width="50%" style="background-color: beige;">
当前证书品牌:<span class="showtype"></span> 域名:<span class="showdomain"></span>
</div>
<center>
<h3>最后一步!</h3>
<p>请将<pre>http://<span class="showdomain"></span>/.well-known/acme-challenge/</pre>重定向到 <pre>http://httpacme.lab.laysense.cn/acme/<span class="showdomain"></span></pre></p>
<button id="checkbutton" onclick="check()">😀我搞定了!</button><br />
<pre id="checknotice" style="background-color: antiquewhite;display: none;"></pre>
<hr><a>🧐不会操作? 查看文档!</a>
</center>
</div>
<div id="step4" style="display: none;">
<div width="50%" style="background-color: beige;">
当前证书品牌:<span class="showtype"></span> 域名:<span class="showdomain"></span>
</div>
<center>
<h3>😍激动人心的时刻</h3>
<button style="background: linear-gradient(135deg, #ff9a9e, #fad0c4);padding:12px;" onclick="skipto('step999')">获取证书</button>
</center>
</div>
<div id="step99" style="display: none;">
<center>
<h3>😳诶呀,所选证书品牌暂时不可用</h3>
<button id="back" onclick="skipto('step2')">🔃重选证书</button>
</center>
</div>
<div id="step999" style="display: none;">
<center>
<h3>😳诶呀,该证书品牌拒绝新的申请</h3>
<button id="back" onclick="skipto('step2')">🔃重选证书</button>
</center>
</div>
<hr />
<div class="footer">
<a>📥下载来笙根证书</a> <a href="https://5050net.cn/fwnet-ecdsa-root-ca-1.crt" target="_blank">📥下载FWNET根证书</a>
<a style="float: right;">📄文档</a>
<a style="float: right;">⏰自动续期</a>
</div>
</div>
<script src="jquery-3.7.1.js"></script>
<script>
changestep();
window.onhashchange=function(){
changestep();
}
function skipto(whichstep){
location.hash=whichstep;
changestep();
}
function choose(type){
window.certtype=type;
location.hash='step3';
changestep();
}
function changestep(){
var domain=$('#domain').val();
var step=location.hash;
if(typeof(step) == "undefined" || !step || !domain || typeof(domain) == "undefined"){
step='#step1';
}else if(step!='#step2'&& !window.certtype || typeof(window.certtype) == "undefined"){
step='#step2';
}else{
$('.showdomain').html(domain);
$('.showtype').html(window.certtype);
}
if(step=='#step4'&&(window.check!='pass'||!window.check)){
step='#step3';
}
//console.log(step);
$(window.laststep).hide();
$(step).show();
window.laststep=step;
}
function check(){
var domain=$('#domain').val();
var email=$('#email').val();
$.ajax({
type: "POST",
url: '/apply/check',
data: {'domain':domain,'email':email,'cert':window.certtype},
async: true,
dataType: 'json',
cache: false,
beforeSend: function () {
$('#checkbutton').attr("disabled","disabled");
$('#checkbutton').hide();
$('#checknotice').show();
$('#checknotice').html('📑检查中,请稍等');
},
success: function (data) {
if (data.code == 200) {
$('#checknotice').html('已通过校验');
changestep('step4');
window.check='pass';
} else {
$('#checkbutton').show();
$('#checknotice').html('校验未通过:'+data.msg);
$('#checkbutton').removeAttr("disabled");
}
},
clearForm: true,
resetForm: false
});
}
</script>
</body>
</html>

60
cacme/composer.json Normal file
View File

@ -0,0 +1,60 @@
{
"name": "workerman/webman",
"type": "project",
"keywords": [
"high performance",
"http service"
],
"homepage": "https://www.workerman.net",
"license": "MIT",
"description": "High performance HTTP Service Framework.",
"authors": [
{
"name": "walkor",
"email": "walkor@workerman.net",
"homepage": "https://www.workerman.net",
"role": "Developer"
}
],
"support": {
"email": "walkor@workerman.net",
"issues": "https://github.com/walkor/webman/issues",
"forum": "https://wenda.workerman.net/",
"wiki": "https://workerman.net/doc/webman",
"source": "https://github.com/walkor/webman"
},
"require": {
"php": ">=7.2",
"workerman/webman-framework": "^1.5.0",
"monolog/monolog": "^2.0",
"webman/console": "^1.3",
"acmephp/core": "^2.1",
"yzh52521/easyhttp": "^1.1",
"workerman/validation": "^3.1"
},
"suggest": {
"ext-event": "For better performance. "
},
"autoload": {
"psr-4": {
"": "./",
"app\\": "./app",
"App\\": "./app",
"app\\View\\Components\\": "./app/view/components"
},
"files": [
"./support/helpers.php"
]
},
"scripts": {
"post-package-install": [
"support\\Plugin::install"
],
"post-package-update": [
"support\\Plugin::install"
],
"pre-package-uninstall": [
"support\\Plugin::uninstall"
]
}
}

2516
cacme/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

26
cacme/config/app.php Normal file
View File

@ -0,0 +1,26 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use support\Request;
return [
'debug' => true,
'error_reporting' => E_ALL,
'default_timezone' => 'Asia/Shanghai',
'request_class' => Request::class,
'public_path' => base_path() . DIRECTORY_SEPARATOR . 'public',
'runtime_path' => base_path(false) . DIRECTORY_SEPARATOR . 'runtime',
'controller_suffix' => 'Controller',
'controller_reuse' => false,
];

21
cacme/config/autoload.php Normal file
View File

@ -0,0 +1,21 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [
'files' => [
base_path() . '/app/functions.php',
base_path() . '/support/Request.php',
base_path() . '/support/Response.php',
]
];

View File

@ -0,0 +1,18 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [
support\bootstrap\Session::class,
support\bootstrap\LaravelDb::class,
];

View File

@ -0,0 +1,15 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return new Webman\Container;

15
cacme/config/database.php Normal file
View File

@ -0,0 +1,15 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [];

View File

@ -0,0 +1,15 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [];

View File

@ -0,0 +1,17 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [
'' => support\exception\Handler::class,
];

32
cacme/config/log.php Normal file
View File

@ -0,0 +1,32 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [
'default' => [
'handlers' => [
[
'class' => Monolog\Handler\RotatingFileHandler::class,
'constructor' => [
runtime_path() . '/logs/webman.log',
7, //$maxFiles
Monolog\Logger::DEBUG,
],
'formatter' => [
'class' => Monolog\Formatter\LineFormatter::class,
'constructor' => [null, 'Y-m-d H:i:s', true],
],
]
],
],
];

View File

@ -0,0 +1,15 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [];

View File

@ -0,0 +1,24 @@
<?php
return [
'enable' => true,
'build_dir' => BASE_PATH . DIRECTORY_SEPARATOR . 'build',
'phar_filename' => 'webman.phar',
'bin_filename' => 'webman.bin',
'signature_algorithm'=> Phar::SHA256, //set the signature algorithm for a phar and apply it. The signature algorithm must be one of Phar::MD5, Phar::SHA1, Phar::SHA256, Phar::SHA512, or Phar::OPENSSL.
'private_key_file' => '', // The file path for certificate or OpenSSL private key file.
'exclude_pattern' => '#^(?!.*(composer.json|/.github/|/.idea/|/.git/|/.setting/|/runtime/|/vendor-bin/|/build/|/vendor/webman/admin/))(.*)$#',
'exclude_files' => [
'.env', 'LICENSE', 'composer.json', 'composer.lock', 'start.php', 'webman.phar', 'webman.bin'
],
'custom_ini' => '
memory_limit = 256M
',
];

42
cacme/config/process.php Normal file
View File

@ -0,0 +1,42 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
global $argv;
return [
// File update detection and automatic reload
'monitor' => [
'handler' => process\Monitor::class,
'reloadable' => false,
'constructor' => [
// Monitor these directories
'monitorDir' => array_merge([
app_path(),
config_path(),
base_path() . '/process',
base_path() . '/support',
base_path() . '/resource',
base_path() . '/.env',
], glob(base_path() . '/plugin/*/app'), glob(base_path() . '/plugin/*/config'), glob(base_path() . '/plugin/*/api')),
// Files with these suffixes will be monitored
'monitorExtensions' => [
'php', 'html', 'htm', 'env'
],
'options' => [
'enable_file_monitor' => !in_array('-d', $argv) && DIRECTORY_SEPARATOR === '/',
'enable_memory_monitor' => DIRECTORY_SEPARATOR === '/',
]
]
]
];

22
cacme/config/redis.php Normal file
View File

@ -0,0 +1,22 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [
'default' => [
'host' => '127.0.0.1',
'password' => null,
'port' => 6379,
'database' => 0,
],
];

22
cacme/config/route.php Normal file
View File

@ -0,0 +1,22 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use Webman\Route;
Route::any('/apply/check', [app\controller\Apply::class, 'check']);

31
cacme/config/server.php Normal file
View File

@ -0,0 +1,31 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [
'listen' => 'http://0.0.0.0:9961',
'transport' => 'tcp',
'context' => [],
'name' => 'webman',
'count' => cpu_count() * 4,
'user' => '',
'group' => '',
'reusePort' => false,
'event_loop' => '',
'stop_timeout' => 2,
'pid_file' => runtime_path() . '/webman.pid',
'status_file' => runtime_path() . '/webman.status',
'stdout_file' => runtime_path() . '/logs/stdout.log',
'log_file' => runtime_path() . '/logs/workerman.log',
'max_package_size' => 10 * 1024 * 1024
];

65
cacme/config/session.php Normal file
View File

@ -0,0 +1,65 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use Webman\Session\FileSessionHandler;
use Webman\Session\RedisSessionHandler;
use Webman\Session\RedisClusterSessionHandler;
return [
'type' => 'file', // or redis or redis_cluster
'handler' => FileSessionHandler::class,
'config' => [
'file' => [
'save_path' => runtime_path() . '/sessions',
],
'redis' => [
'host' => '127.0.0.1',
'port' => 6379,
'auth' => '',
'timeout' => 2,
'database' => '',
'prefix' => 'redis_session_',
],
'redis_cluster' => [
'host' => ['127.0.0.1:7000', '127.0.0.1:7001', '127.0.0.1:7001'],
'timeout' => 2,
'auth' => '',
'prefix' => 'redis_session_',
]
],
'session_name' => 'PHPSID',
'auto_update_timestamp' => false,
'lifetime' => 7*24*60*60,
'cookie_lifetime' => 365*24*60*60,
'cookie_path' => '/',
'domain' => '',
'http_only' => true,
'secure' => false,
'same_site' => '',
'gc_probability' => [1, 1000],
];

23
cacme/config/static.php Normal file
View File

@ -0,0 +1,23 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
/**
* Static file settings
*/
return [
'enable' => true,
'middleware' => [ // Static file Middleware
//app\middleware\StaticFile::class,
],
];

View File

@ -0,0 +1,25 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
/**
* Multilingual configuration
*/
return [
// Default language
'locale' => 'zh_CN',
// Fallback language
'fallback_locale' => ['zh_CN', 'en'],
// Folder where language files are stored
'path' => base_path() . '/resource/translations',
];

22
cacme/config/view.php Normal file
View File

@ -0,0 +1,22 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use support\view\Raw;
use support\view\Twig;
use support\view\Blade;
use support\view\ThinkPHP;
return [
'handler' => Raw::class
];

243
cacme/process/Monitor.php Normal file
View File

@ -0,0 +1,243 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace process;
use FilesystemIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SplFileInfo;
use Workerman\Timer;
use Workerman\Worker;
/**
* Class FileMonitor
* @package process
*/
class Monitor
{
/**
* @var array
*/
protected $paths = [];
/**
* @var array
*/
protected $extensions = [];
/**
* @var string
*/
public static $lockFile = __DIR__ . '/../runtime/monitor.lock';
/**
* Pause monitor
* @return void
*/
public static function pause()
{
file_put_contents(static::$lockFile, time());
}
/**
* Resume monitor
* @return void
*/
public static function resume(): void
{
clearstatcache();
if (is_file(static::$lockFile)) {
unlink(static::$lockFile);
}
}
/**
* Whether monitor is paused
* @return bool
*/
public static function isPaused(): bool
{
clearstatcache();
return file_exists(static::$lockFile);
}
/**
* FileMonitor constructor.
* @param $monitorDir
* @param $monitorExtensions
* @param array $options
*/
public function __construct($monitorDir, $monitorExtensions, array $options = [])
{
static::resume();
$this->paths = (array)$monitorDir;
$this->extensions = $monitorExtensions;
if (!Worker::getAllWorkers()) {
return;
}
$disableFunctions = explode(',', ini_get('disable_functions'));
if (in_array('exec', $disableFunctions, true)) {
echo "\nMonitor file change turned off because exec() has been disabled by disable_functions setting in " . PHP_CONFIG_FILE_PATH . "/php.ini\n";
} else {
if ($options['enable_file_monitor'] ?? true) {
Timer::add(1, function () {
$this->checkAllFilesChange();
});
}
}
$memoryLimit = $this->getMemoryLimit($options['memory_limit'] ?? null);
if ($memoryLimit && ($options['enable_memory_monitor'] ?? true)) {
Timer::add(60, [$this, 'checkMemory'], [$memoryLimit]);
}
}
/**
* @param $monitorDir
* @return bool
*/
public function checkFilesChange($monitorDir): bool
{
static $lastMtime, $tooManyFilesCheck;
if (!$lastMtime) {
$lastMtime = time();
}
clearstatcache();
if (!is_dir($monitorDir)) {
if (!is_file($monitorDir)) {
return false;
}
$iterator = [new SplFileInfo($monitorDir)];
} else {
// recursive traversal directory
$dirIterator = new RecursiveDirectoryIterator($monitorDir, FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS);
$iterator = new RecursiveIteratorIterator($dirIterator);
}
$count = 0;
foreach ($iterator as $file) {
$count ++;
/** var SplFileInfo $file */
if (is_dir($file->getRealPath())) {
continue;
}
// check mtime
if (in_array($file->getExtension(), $this->extensions, true) && $lastMtime < $file->getMTime()) {
$var = 0;
exec('"'.PHP_BINARY . '" -l ' . $file, $out, $var);
$lastMtime = $file->getMTime();
if ($var) {
continue;
}
echo $file . " update and reload\n";
// send SIGUSR1 signal to master process for reload
if (DIRECTORY_SEPARATOR === '/') {
posix_kill(posix_getppid(), SIGUSR1);
} else {
return true;
}
break;
}
}
if (!$tooManyFilesCheck && $count > 1000) {
echo "Monitor: There are too many files ($count files) in $monitorDir which makes file monitoring very slow\n";
$tooManyFilesCheck = 1;
}
return false;
}
/**
* @return bool
*/
public function checkAllFilesChange(): bool
{
if (static::isPaused()) {
return false;
}
foreach ($this->paths as $path) {
if ($this->checkFilesChange($path)) {
return true;
}
}
return false;
}
/**
* @param $memoryLimit
* @return void
*/
public function checkMemory($memoryLimit)
{
if (static::isPaused() || $memoryLimit <= 0) {
return;
}
$ppid = posix_getppid();
$childrenFile = "/proc/$ppid/task/$ppid/children";
if (!is_file($childrenFile) || !($children = file_get_contents($childrenFile))) {
return;
}
foreach (explode(' ', $children) as $pid) {
$pid = (int)$pid;
$statusFile = "/proc/$pid/status";
if (!is_file($statusFile) || !($status = file_get_contents($statusFile))) {
continue;
}
$mem = 0;
if (preg_match('/VmRSS\s*?:\s*?(\d+?)\s*?kB/', $status, $match)) {
$mem = $match[1];
}
$mem = (int)($mem / 1024);
if ($mem >= $memoryLimit) {
posix_kill($pid, SIGINT);
}
}
}
/**
* Get memory limit
* @return float
*/
protected function getMemoryLimit($memoryLimit)
{
if ($memoryLimit === 0) {
return 0;
}
$usePhpIni = false;
if (!$memoryLimit) {
$memoryLimit = ini_get('memory_limit');
$usePhpIni = true;
}
if ($memoryLimit == -1) {
return 0;
}
$unit = strtolower($memoryLimit[strlen($memoryLimit) - 1]);
if ($unit === 'g') {
$memoryLimit = 1024 * (int)$memoryLimit;
} else if ($unit === 'm') {
$memoryLimit = (int)$memoryLimit;
} else if ($unit === 'k') {
$memoryLimit = ((int)$memoryLimit / 1024);
} else {
$memoryLimit = ((int)$memoryLimit / (1024 * 1024));
}
if ($memoryLimit < 30) {
$memoryLimit = 30;
}
if ($usePhpIni) {
$memoryLimit = (int)(0.8 * $memoryLimit);
}
return $memoryLimit;
}
}

12
cacme/public/404.html Normal file
View File

@ -0,0 +1,12 @@
<html>
<head>
<title>404 Not Found - webman</title>
</head>
<body>
<center>
<h1>404 Not Found</h1>
</center>
<hr>
<center>webman</center>
</body>
</html>

BIN
cacme/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

10716
cacme/public/jquery-3.7.1.js vendored Normal file

File diff suppressed because it is too large Load Diff

461
cacme/public/normal.css Normal file
View File

@ -0,0 +1,461 @@
/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */
/**
* 1. Change the default font family in all browsers (opinionated).
* 2. Correct the line height in all browsers.
* 3. Prevent adjustments of font size after orientation changes in
* IE on Windows Phone and in iOS.
*/
/* Document
========================================================================== */
html {
font-family: sans-serif; /* 1 */
line-height: 1.15; /* 2 */
-ms-text-size-adjust: 100%; /* 3 */
-webkit-text-size-adjust: 100%; /* 3 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers (opinionated).
*/
body {
margin: 0;
}
/**
* Add the correct display in IE 9-.
*/
article,
aside,
footer,
header,
nav,
section {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* Add the correct display in IE 9-.
* 1. Add the correct display in IE.
*/
figcaption,
figure,
main { /* 1 */
display: block;
}
/**
* Add the correct margin in IE 8.
*/
figure {
margin: 1em 40px;
}
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* 1. Remove the gray background on active links in IE 10.
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
*/
a {
background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */
}
/**
* Remove the outline on focused links when they are also active or hovered
* in all browsers (opinionated).
*/
a:active,
a:hover {
outline-width: 0;
}
/**
* 1. Remove the bottom border in Firefox 39-.
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
*/
b,
strong {
font-weight: inherit;
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font style in Android 4.3-.
*/
dfn {
font-style: italic;
}
/**
* Add the correct background and color in IE 9-.
*/
mark {
background-color: #ff0;
color: #000;
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
audio,
video {
display: inline-block;
}
/**
* Add the correct display in iOS 4-7.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Remove the border on images inside links in IE 10-.
*/
img {
border-style: none;
}
/**
* Hide the overflow in IE.
*/
svg:not(:root) {
overflow: hidden;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers (opinionated).
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: sans-serif; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
* controls in Android 4.
* 2. Correct the inability to style clickable types in iOS and Safari.
*/
button,
html [type="button"], /* 1 */
[type="reset"],
[type="submit"] {
-webkit-appearance: button; /* 2 */
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Change the border, margin, and padding in all browsers (opinionated).
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* 1. Add the correct display in IE 9-.
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Remove the default vertical scrollbar in IE.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10-.
* 2. Remove the padding in IE 10-.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in IE 9-.
* 1. Add the correct display in Edge, IE, and Firefox.
*/
details, /* 1 */
menu {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Scripting
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
canvas {
display: inline-block;
}
/**
* Add the correct display in IE.
*/
template {
display: none;
}
/* Hidden
========================================================================== */
/**
* Add the correct display in IE 10-.
*/
[hidden] {
display: none;
}

225
cacme/public/sakura.css Normal file
View File

@ -0,0 +1,225 @@
/* Sakura.css v1.5.0
* ================
* Minimal css theme.
* Project: https://github.com/oxalorg/sakura/
*/
/* Body */
html {
font-size: 62.5%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
}
body {
font-size: 1.8rem;
line-height: 1.618;
max-width: 38em;
margin: auto;
color: #49002d;
padding: 13px;
}
@media (max-width: 684px) {
body {
font-size: 1.53rem;
}
}
@media (max-width: 382px) {
body {
font-size: 1.35rem;
}
}
h1, h2, h3, h4, h5, h6 {
line-height: 1.1;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
font-weight: 700;
margin-top: 3rem;
margin-bottom: 1.5rem;
overflow-wrap: break-word;
word-wrap: break-word;
-ms-word-break: break-all;
word-break: break-word;
}
h1 {
font-size: 2.35em;
}
h2 {
font-size: 2em;
}
h3 {
font-size: 1.75em;
}
h4 {
font-size: 1.5em;
}
h5 {
font-size: 1.25em;
}
h6 {
font-size: 1em;
}
p {
margin-top: 0px;
margin-bottom: 2.5rem;
}
small, sub, sup {
font-size: 75%;
}
hr {
border-color: #980255;
}
a {
text-decoration: none;
color: #980255;
}
a:visited {
color: #660139;
}
a:hover {
color: #753851;
border-bottom: 2px solid #49002d;
}
ul {
padding-left: 1.4em;
margin-top: 0px;
margin-bottom: 2.5rem;
}
li {
margin-bottom: 0.4em;
}
blockquote {
margin-left: 0px;
margin-right: 0px;
padding-left: 1em;
padding-top: 0.8em;
padding-bottom: 0.8em;
padding-right: 0.8em;
border-left: 5px solid #980255;
margin-bottom: 2.5rem;
background-color: #f8d2e9;
}
blockquote p {
margin-bottom: 0;
}
img, video {
height: auto;
max-width: 100%;
margin-top: 0px;
margin-bottom: 2.5rem;
}
/* Pre and Code */
pre {
background-color: #f8d2e9;
display: block;
padding: 1em;
overflow-x: auto;
margin-top: 0px;
margin-bottom: 2.5rem;
font-size: 0.9em;
}
code, kbd, samp {
font-size: 0.9em;
padding: 0 0.5em;
background-color: #f8d2e9;
white-space: pre-wrap;
}
pre > code {
padding: 0;
background-color: transparent;
white-space: pre;
font-size: 1em;
}
/* Tables */
table {
text-align: justify;
width: 100%;
border-collapse: collapse;
margin-bottom: 2rem;
}
td, th {
padding: 0.5em;
border-bottom: 1px solid #f8d2e9;
}
/* Buttons, forms and input */
input, textarea {
border: 1px solid #49002d;
}
input:focus, textarea:focus {
border: 1px solid #980255;
}
textarea {
width: 100%;
}
.button, button, input[type=submit], input[type=reset], input[type=button], input[type=file]::file-selector-button {
display: inline-block;
padding: 5px 10px;
text-align: center;
text-decoration: none;
white-space: nowrap;
background-color: #980255;
color: #ffe4f5;
border-radius: 1px;
border: 1px solid #980255;
cursor: pointer;
box-sizing: border-box;
}
.button[disabled], button[disabled], input[type=submit][disabled], input[type=reset][disabled], input[type=button][disabled], input[type=file]::file-selector-button[disabled] {
cursor: default;
opacity: 0.5;
}
.button:hover, button:hover, input[type=submit]:hover, input[type=reset]:hover, input[type=button]:hover, input[type=file]::file-selector-button:hover {
background-color: #753851;
color: #ffe4f5;
outline: 0;
}
.button:focus-visible, button:focus-visible, input[type=submit]:focus-visible, input[type=reset]:focus-visible, input[type=button]:focus-visible, input[type=file]::file-selector-button:focus-visible {
outline-style: solid;
outline-width: 2px;
}
textarea, select, input {
color: #49002d;
padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
margin-bottom: 10px;
background-color: #f8d2e9;
border: 1px solid #f8d2e9;
border-radius: 4px;
box-shadow: none;
box-sizing: border-box;
}
textarea:focus, select:focus, input:focus {
border: 1px solid #980255;
outline: 0;
}
input[type=checkbox]:focus {
outline: 1px dotted #980255;
}
label, legend, fieldset {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
}

4
cacme/runtime/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*
!logs
!views
!.gitignore

2
cacme/runtime/logs/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

2
cacme/runtime/views/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

4
cacme/start.php Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env php
<?php
require_once __DIR__ . '/vendor/autoload.php';
support\App::run();

24
cacme/support/Request.php Normal file
View File

@ -0,0 +1,24 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support;
/**
* Class Request
* @package support
*/
class Request extends \Webman\Http\Request
{
}

View File

@ -0,0 +1,24 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support;
/**
* Class Response
* @package support
*/
class Response extends \Webman\Http\Response
{
}

133
cacme/support/bootstrap.php Normal file
View File

@ -0,0 +1,133 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use Dotenv\Dotenv;
use support\Log;
use Webman\Bootstrap;
use Webman\Config;
use Webman\Middleware;
use Webman\Route;
use Webman\Util;
$worker = $worker ?? null;
set_error_handler(function ($level, $message, $file = '', $line = 0) {
if (error_reporting() & $level) {
throw new ErrorException($message, 0, $level, $file, $line);
}
});
if ($worker) {
register_shutdown_function(function ($startTime) {
if (time() - $startTime <= 0.1) {
sleep(1);
}
}, time());
}
if (class_exists('Dotenv\Dotenv') && file_exists(base_path(false) . '/.env')) {
if (method_exists('Dotenv\Dotenv', 'createUnsafeMutable')) {
Dotenv::createUnsafeMutable(base_path(false))->load();
} else {
Dotenv::createMutable(base_path(false))->load();
}
}
Config::clear();
support\App::loadAllConfig(['route']);
if ($timezone = config('app.default_timezone')) {
date_default_timezone_set($timezone);
}
foreach (config('autoload.files', []) as $file) {
include_once $file;
}
foreach (config('plugin', []) as $firm => $projects) {
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['autoload']['files'] ?? [] as $file) {
include_once $file;
}
}
foreach ($projects['autoload']['files'] ?? [] as $file) {
include_once $file;
}
}
Middleware::load(config('middleware', []));
foreach (config('plugin', []) as $firm => $projects) {
foreach ($projects as $name => $project) {
if (!is_array($project) || $name === 'static') {
continue;
}
Middleware::load($project['middleware'] ?? []);
}
Middleware::load($projects['middleware'] ?? [], $firm);
if ($staticMiddlewares = config("plugin.$firm.static.middleware")) {
Middleware::load(['__static__' => $staticMiddlewares], $firm);
}
}
Middleware::load(['__static__' => config('static.middleware', [])]);
foreach (config('bootstrap', []) as $className) {
if (!class_exists($className)) {
$log = "Warning: Class $className setting in config/bootstrap.php not found\r\n";
echo $log;
Log::error($log);
continue;
}
/** @var Bootstrap $className */
$className::start($worker);
}
foreach (config('plugin', []) as $firm => $projects) {
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['bootstrap'] ?? [] as $className) {
if (!class_exists($className)) {
$log = "Warning: Class $className setting in config/plugin/$firm/$name/bootstrap.php not found\r\n";
echo $log;
Log::error($log);
continue;
}
/** @var Bootstrap $className */
$className::start($worker);
}
}
foreach ($projects['bootstrap'] ?? [] as $className) {
/** @var string $className */
if (!class_exists($className)) {
$log = "Warning: Class $className setting in plugin/$firm/config/bootstrap.php not found\r\n";
echo $log;
Log::error($log);
continue;
}
/** @var Bootstrap $className */
$className::start($worker);
}
}
$directory = base_path() . '/plugin';
$paths = [config_path()];
foreach (Util::scanDir($directory) as $path) {
if (is_dir($path = "$path/config")) {
$paths[] = $path;
}
}
Route::load($paths);

528
cacme/support/helpers.php Normal file
View File

@ -0,0 +1,528 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use support\Container;
use support\Request;
use support\Response;
use support\Translation;
use support\view\Blade;
use support\view\Raw;
use support\view\ThinkPHP;
use support\view\Twig;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
use Webman\App;
use Webman\Config;
use Webman\Route;
use Workerman\Protocols\Http\Session;
use Workerman\Worker;
// Project base path
define('BASE_PATH', dirname(__DIR__));
/**
* return the program execute directory
* @param string $path
* @return string
*/
function run_path(string $path = ''): string
{
static $runPath = '';
if (!$runPath) {
$runPath = is_phar() ? dirname(Phar::running(false)) : BASE_PATH;
}
return path_combine($runPath, $path);
}
/**
* if the param $path equal false,will return this program current execute directory
* @param string|false $path
* @return string
*/
function base_path($path = ''): string
{
if (false === $path) {
return run_path();
}
return path_combine(BASE_PATH, $path);
}
/**
* App path
* @param string $path
* @return string
*/
function app_path(string $path = ''): string
{
return path_combine(BASE_PATH . DIRECTORY_SEPARATOR . 'app', $path);
}
/**
* Public path
* @param string $path
* @return string
*/
function public_path(string $path = ''): string
{
static $publicPath = '';
if (!$publicPath) {
$publicPath = \config('app.public_path') ?: run_path('public');
}
return path_combine($publicPath, $path);
}
/**
* Config path
* @param string $path
* @return string
*/
function config_path(string $path = ''): string
{
return path_combine(BASE_PATH . DIRECTORY_SEPARATOR . 'config', $path);
}
/**
* Runtime path
* @param string $path
* @return string
*/
function runtime_path(string $path = ''): string
{
static $runtimePath = '';
if (!$runtimePath) {
$runtimePath = \config('app.runtime_path') ?: run_path('runtime');
}
return path_combine($runtimePath, $path);
}
/**
* Generate paths based on given information
* @param string $front
* @param string $back
* @return string
*/
function path_combine(string $front, string $back): string
{
return $front . ($back ? (DIRECTORY_SEPARATOR . ltrim($back, DIRECTORY_SEPARATOR)) : $back);
}
/**
* Response
* @param int $status
* @param array $headers
* @param string $body
* @return Response
*/
function response(string $body = '', int $status = 200, array $headers = []): Response
{
return new Response($status, $headers, $body);
}
/**
* Json response
* @param $data
* @param int $options
* @return Response
*/
function json($data, int $options = JSON_UNESCAPED_UNICODE): Response
{
return new Response(200, ['Content-Type' => 'application/json'], json_encode($data, $options));
}
/**
* Xml response
* @param $xml
* @return Response
*/
function xml($xml): Response
{
if ($xml instanceof SimpleXMLElement) {
$xml = $xml->asXML();
}
return new Response(200, ['Content-Type' => 'text/xml'], $xml);
}
/**
* Jsonp response
* @param $data
* @param string $callbackName
* @return Response
*/
function jsonp($data, string $callbackName = 'callback'): Response
{
if (!is_scalar($data) && null !== $data) {
$data = json_encode($data);
}
return new Response(200, [], "$callbackName($data)");
}
/**
* Redirect response
* @param string $location
* @param int $status
* @param array $headers
* @return Response
*/
function redirect(string $location, int $status = 302, array $headers = []): Response
{
$response = new Response($status, ['Location' => $location]);
if (!empty($headers)) {
$response->withHeaders($headers);
}
return $response;
}
/**
* View response
* @param string $template
* @param array $vars
* @param string|null $app
* @param string|null $plugin
* @return Response
*/
function view(string $template, array $vars = [], string $app = null, string $plugin = null): Response
{
$request = \request();
$plugin = $plugin === null ? ($request->plugin ?? '') : $plugin;
$handler = \config($plugin ? "plugin.$plugin.view.handler" : 'view.handler');
return new Response(200, [], $handler::render($template, $vars, $app, $plugin));
}
/**
* Raw view response
* @param string $template
* @param array $vars
* @param string|null $app
* @return Response
* @throws Throwable
*/
function raw_view(string $template, array $vars = [], string $app = null): Response
{
return new Response(200, [], Raw::render($template, $vars, $app));
}
/**
* Blade view response
* @param string $template
* @param array $vars
* @param string|null $app
* @return Response
*/
function blade_view(string $template, array $vars = [], string $app = null): Response
{
return new Response(200, [], Blade::render($template, $vars, $app));
}
/**
* Think view response
* @param string $template
* @param array $vars
* @param string|null $app
* @return Response
*/
function think_view(string $template, array $vars = [], string $app = null): Response
{
return new Response(200, [], ThinkPHP::render($template, $vars, $app));
}
/**
* Twig view response
* @param string $template
* @param array $vars
* @param string|null $app
* @return Response
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
function twig_view(string $template, array $vars = [], string $app = null): Response
{
return new Response(200, [], Twig::render($template, $vars, $app));
}
/**
* Get request
* @return \Webman\Http\Request|Request|null
*/
function request()
{
return App::request();
}
/**
* Get config
* @param string|null $key
* @param $default
* @return array|mixed|null
*/
function config(string $key = null, $default = null)
{
return Config::get($key, $default);
}
/**
* Create url
* @param string $name
* @param ...$parameters
* @return string
*/
function route(string $name, ...$parameters): string
{
$route = Route::getByName($name);
if (!$route) {
return '';
}
if (!$parameters) {
return $route->url();
}
if (is_array(current($parameters))) {
$parameters = current($parameters);
}
return $route->url($parameters);
}
/**
* Session
* @param mixed $key
* @param mixed $default
* @return mixed|bool|Session
*/
function session($key = null, $default = null)
{
$session = \request()->session();
if (null === $key) {
return $session;
}
if (is_array($key)) {
$session->put($key);
return null;
}
if (strpos($key, '.')) {
$keyArray = explode('.', $key);
$value = $session->all();
foreach ($keyArray as $index) {
if (!isset($value[$index])) {
return $default;
}
$value = $value[$index];
}
return $value;
}
return $session->get($key, $default);
}
/**
* Translation
* @param string $id
* @param array $parameters
* @param string|null $domain
* @param string|null $locale
* @return string
*/
function trans(string $id, array $parameters = [], string $domain = null, string $locale = null): string
{
$res = Translation::trans($id, $parameters, $domain, $locale);
return $res === '' ? $id : $res;
}
/**
* Locale
* @param string|null $locale
* @return string
*/
function locale(string $locale = null): string
{
if (!$locale) {
return Translation::getLocale();
}
Translation::setLocale($locale);
return $locale;
}
/**
* 404 not found
* @return Response
*/
function not_found(): Response
{
return new Response(404, [], file_get_contents(public_path() . '/404.html'));
}
/**
* Copy dir
* @param string $source
* @param string $dest
* @param bool $overwrite
* @return void
*/
function copy_dir(string $source, string $dest, bool $overwrite = false)
{
if (is_dir($source)) {
if (!is_dir($dest)) {
mkdir($dest);
}
$files = scandir($source);
foreach ($files as $file) {
if ($file !== "." && $file !== "..") {
copy_dir("$source/$file", "$dest/$file", $overwrite);
}
}
} else if (file_exists($source) && ($overwrite || !file_exists($dest))) {
copy($source, $dest);
}
}
/**
* Remove dir
* @param string $dir
* @return bool
*/
function remove_dir(string $dir): bool
{
if (is_link($dir) || is_file($dir)) {
return unlink($dir);
}
$files = array_diff(scandir($dir), array('.', '..'));
foreach ($files as $file) {
(is_dir("$dir/$file") && !is_link($dir)) ? remove_dir("$dir/$file") : unlink("$dir/$file");
}
return rmdir($dir);
}
/**
* Bind worker
* @param $worker
* @param $class
*/
function worker_bind($worker, $class)
{
$callbackMap = [
'onConnect',
'onMessage',
'onClose',
'onError',
'onBufferFull',
'onBufferDrain',
'onWorkerStop',
'onWebSocketConnect',
'onWorkerReload'
];
foreach ($callbackMap as $name) {
if (method_exists($class, $name)) {
$worker->$name = [$class, $name];
}
}
if (method_exists($class, 'onWorkerStart')) {
call_user_func([$class, 'onWorkerStart'], $worker);
}
}
/**
* Start worker
* @param $processName
* @param $config
* @return void
*/
function worker_start($processName, $config)
{
$worker = new Worker($config['listen'] ?? null, $config['context'] ?? []);
$propertyMap = [
'count',
'user',
'group',
'reloadable',
'reusePort',
'transport',
'protocol',
];
$worker->name = $processName;
foreach ($propertyMap as $property) {
if (isset($config[$property])) {
$worker->$property = $config[$property];
}
}
$worker->onWorkerStart = function ($worker) use ($config) {
require_once base_path('/support/bootstrap.php');
if (isset($config['handler'])) {
if (!class_exists($config['handler'])) {
echo "process error: class {$config['handler']} not exists\r\n";
return;
}
$instance = Container::make($config['handler'], $config['constructor'] ?? []);
worker_bind($worker, $instance);
}
};
}
/**
* Get realpath
* @param string $filePath
* @return string
*/
function get_realpath(string $filePath): string
{
if (strpos($filePath, 'phar://') === 0) {
return $filePath;
} else {
return realpath($filePath);
}
}
/**
* Is phar
* @return bool
*/
function is_phar(): bool
{
return class_exists(Phar::class, false) && Phar::running();
}
/**
* Get cpu count
* @return int
*/
function cpu_count(): int
{
// Windows does not support the number of processes setting.
if (DIRECTORY_SEPARATOR === '\\') {
return 1;
}
$count = 4;
if (is_callable('shell_exec')) {
if (strtolower(PHP_OS) === 'darwin') {
$count = (int)shell_exec('sysctl -n machdep.cpu.core_count');
} else {
$count = (int)shell_exec('nproc');
}
}
return $count > 0 ? $count : 4;
}
/**
* Get request parameters, if no parameter name is passed, an array of all values is returned, default values is supported
* @param string|null $param param's name
* @param mixed|null $default default value
* @return mixed|null
*/
function input(string $param = null, $default = null)
{
return is_null($param) ? request()->all() : request()->input($param, $default);
}

2
cacme/vendor/acmephp/core/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
composer.lock
vendor/

404
cacme/vendor/acmephp/core/AcmeClient.php vendored Normal file
View File

@ -0,0 +1,404 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core;
use AcmePhp\Core\Exception\AcmeCoreClientException;
use AcmePhp\Core\Exception\AcmeCoreServerException;
use AcmePhp\Core\Exception\Protocol\CertificateRequestFailedException;
use AcmePhp\Core\Exception\Protocol\CertificateRevocationException;
use AcmePhp\Core\Exception\Protocol\ChallengeFailedException;
use AcmePhp\Core\Exception\Protocol\ChallengeNotSupportedException;
use AcmePhp\Core\Exception\Protocol\ChallengeTimedOutException;
use AcmePhp\Core\Http\SecureHttpClient;
use AcmePhp\Core\Protocol\AuthorizationChallenge;
use AcmePhp\Core\Protocol\CertificateOrder;
use AcmePhp\Core\Protocol\ExternalAccount;
use AcmePhp\Core\Protocol\ResourcesDirectory;
use AcmePhp\Core\Protocol\RevocationReason;
use AcmePhp\Ssl\Certificate;
use AcmePhp\Ssl\CertificateRequest;
use AcmePhp\Ssl\CertificateResponse;
use AcmePhp\Ssl\Signer\CertificateRequestSigner;
use GuzzleHttp\Psr7\Utils;
use Webmozart\Assert\Assert;
/**
* ACME protocol client implementation.
*
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class AcmeClient implements AcmeClientInterface
{
/**
* @var SecureHttpClient
*/
private $uninitializedHttpClient;
/**
* @var SecureHttpClient
*/
private $initializedHttpClient;
/**
* @var CertificateRequestSigner
*/
private $csrSigner;
/**
* @var string
*/
private $directoryUrl;
/**
* @var ResourcesDirectory
*/
private $directory;
/**
* @var string
*/
private $account;
public function __construct(SecureHttpClient $httpClient, string $directoryUrl, CertificateRequestSigner $csrSigner = null)
{
$this->uninitializedHttpClient = $httpClient;
$this->directoryUrl = $directoryUrl;
$this->csrSigner = $csrSigner ?: new CertificateRequestSigner();
}
/**
* {@inheritdoc}
*/
public function registerAccount(string $email = null, ExternalAccount $externalAccount = null): array
{
$client = $this->getHttpClient();
$payload = [
'termsOfServiceAgreed' => true,
'contact' => [],
];
if ($email) {
$payload['contact'][] = 'mailto:'.$email;
}
if ($externalAccount) {
$payload['externalAccountBinding'] = $client->createExternalAccountPayload(
$externalAccount,
$this->getResourceUrl(ResourcesDirectory::NEW_ACCOUNT)
);
}
$this->requestResource('POST', ResourcesDirectory::NEW_ACCOUNT, $payload);
$account = $this->getResourceAccount();
return $client->request('POST', $account, $client->signKidPayload($account, $account, null));
}
/**
* {@inheritdoc}
*/
public function requestOrder(array $domains): CertificateOrder
{
Assert::allStringNotEmpty($domains, 'requestOrder::$domains expected a list of strings. Got: %s');
$payload = [
'identifiers' => array_map(
static function ($domain) {
return [
'type' => 'dns',
'value' => $domain,
];
},
array_values($domains)
),
];
$client = $this->getHttpClient();
$resourceUrl = $this->getResourceUrl(ResourcesDirectory::NEW_ORDER);
$response = $client->request('POST', $resourceUrl, $client->signKidPayload($resourceUrl, $this->getResourceAccount(), $payload));
if (!isset($response['authorizations']) || !$response['authorizations']) {
throw new ChallengeNotSupportedException();
}
$authorizationsChallenges = [];
$orderEndpoint = $client->getLastLocation();
foreach ($response['authorizations'] as $authorizationEndpoint) {
$authorizationsResponse = $client->request('POST', $authorizationEndpoint, $client->signKidPayload($authorizationEndpoint, $this->getResourceAccount(), null));
$domain = (empty($authorizationsResponse['wildcard']) ? '' : '*.').$authorizationsResponse['identifier']['value'];
foreach ($authorizationsResponse['challenges'] as $challenge) {
$authorizationsChallenges[$domain][] = $this->createAuthorizationChallenge($authorizationsResponse['identifier']['value'], $challenge);
}
}
return new CertificateOrder($authorizationsChallenges, $orderEndpoint, $response['status']);
}
/**
* {@inheritdoc}
*/
public function reloadOrder(CertificateOrder $order): CertificateOrder
{
$client = $this->getHttpClient();
$orderEndpoint = $order->getOrderEndpoint();
$response = $client->request('POST', $orderEndpoint, $client->signKidPayload($orderEndpoint, $this->getResourceAccount(), null));
if (!isset($response['authorizations']) || !$response['authorizations']) {
throw new ChallengeNotSupportedException();
}
$authorizationsChallenges = [];
foreach ($response['authorizations'] as $authorizationEndpoint) {
$authorizationsResponse = $client->request('POST', $authorizationEndpoint, $client->signKidPayload($authorizationEndpoint, $this->getResourceAccount(), null));
$domain = (empty($authorizationsResponse['wildcard']) ? '' : '*.').$authorizationsResponse['identifier']['value'];
foreach ($authorizationsResponse['challenges'] as $challenge) {
$authorizationsChallenges[$domain][] = $this->createAuthorizationChallenge($authorizationsResponse['identifier']['value'], $challenge);
}
}
return new CertificateOrder($authorizationsChallenges, $orderEndpoint, $response['status']);
}
/**
* {@inheritdoc}
*/
public function finalizeOrder(CertificateOrder $order, CertificateRequest $csr, int $timeout = 180, bool $returnAlternateCertificateIfAvailable = false): CertificateResponse
{
$endTime = time() + $timeout;
$client = $this->getHttpClient();
$orderEndpoint = $order->getOrderEndpoint();
$response = $client->request('POST', $orderEndpoint, $client->signKidPayload($orderEndpoint, $this->getResourceAccount(), null));
if (\in_array($response['status'], ['pending', 'processing', 'ready'])) {
$humanText = ['-----BEGIN CERTIFICATE REQUEST-----', '-----END CERTIFICATE REQUEST-----'];
$csrContent = $this->csrSigner->signCertificateRequest($csr);
$csrContent = trim(str_replace($humanText, '', $csrContent));
$csrContent = trim($client->getBase64Encoder()->encode(base64_decode($csrContent)));
$response = $client->request('POST', $response['finalize'], $client->signKidPayload($response['finalize'], $this->getResourceAccount(), ['csr' => $csrContent]));
}
// Waiting loop
while (time() <= $endTime && (!isset($response['status']) || \in_array($response['status'], ['pending', 'processing', 'ready']))) {
sleep(1);
$response = $client->request('POST', $orderEndpoint, $client->signKidPayload($orderEndpoint, $this->getResourceAccount(), null));
}
if ('valid' !== $response['status']) {
throw new CertificateRequestFailedException('The order has not been validated');
}
$response = $client->rawRequest('POST', $response['certificate'], $client->signKidPayload($response['certificate'], $this->getResourceAccount(), null));
$responseHeaders = $response->getHeaders();
if ($returnAlternateCertificateIfAvailable && isset($responseHeaders['Link'][1])) {
$matches = [];
preg_match('/<(http.*)>;rel="alternate"/', $responseHeaders['Link'][1], $matches);
// If response headers include a valid alternate certificate link, return that certificate instead
if (isset($matches[1])) {
return $this->createCertificateResponse(
$csr,
$client->request('POST', $matches[1], $client->signKidPayload($matches[1], $this->getResourceAccount(), null), false)
);
}
}
return $this->createCertificateResponse($csr, Utils::copyToString($response->getBody()));
}
/**
* {@inheritdoc}
*/
public function requestAuthorization(string $domain): array
{
$order = $this->requestOrder([$domain]);
try {
return $order->getAuthorizationChallenges($domain);
} catch (AcmeCoreClientException $e) {
throw new ChallengeNotSupportedException();
}
}
/**
* {@inheritdoc}
*/
public function reloadAuthorization(AuthorizationChallenge $challenge): AuthorizationChallenge
{
$client = $this->getHttpClient();
$challengeUrl = $challenge->getUrl();
$response = (array) $client->request('POST', $challengeUrl, $client->signKidPayload($challengeUrl, $this->getResourceAccount(), null));
return $this->createAuthorizationChallenge($challenge->getDomain(), $response);
}
/**
* {@inheritdoc}
*/
public function challengeAuthorization(AuthorizationChallenge $challenge, int $timeout = 180): array
{
$endTime = time() + $timeout;
$client = $this->getHttpClient();
$challengeUrl = $challenge->getUrl();
$response = (array) $client->request('POST', $challengeUrl, $client->signKidPayload($challengeUrl, $this->getResourceAccount(), null));
if ('pending' === $response['status'] || 'processing' === $response['status']) {
$response = (array) $client->request('POST', $challengeUrl, $client->signKidPayload($challengeUrl, $this->getResourceAccount(), []));
}
// Waiting loop
while (time() <= $endTime && (!isset($response['status']) || 'pending' === $response['status'] || 'processing' === $response['status'])) {
sleep(1);
$response = (array) $client->request('POST', $challengeUrl, $client->signKidPayload($challengeUrl, $this->getResourceAccount(), null));
}
if (isset($response['status']) && ('pending' === $response['status'] || 'processing' === $response['status'])) {
throw new ChallengeTimedOutException($response);
}
if (!isset($response['status']) || 'valid' !== $response['status']) {
throw new ChallengeFailedException($response);
}
return $response;
}
/**
* {@inheritdoc}
*/
public function requestCertificate(string $domain, CertificateRequest $csr, int $timeout = 180, bool $returnAlternateCertificateIfAvailable = false): CertificateResponse
{
$order = $this->requestOrder(array_unique(array_merge([$domain], $csr->getDistinguishedName()->getSubjectAlternativeNames())));
return $this->finalizeOrder($order, $csr, $timeout, $returnAlternateCertificateIfAvailable);
}
/**
* {@inheritdoc}
*/
public function revokeCertificate(Certificate $certificate, RevocationReason $revocationReason = null)
{
if (!$endpoint = $this->getResourceUrl(ResourcesDirectory::REVOKE_CERT)) {
throw new CertificateRevocationException('This ACME server does not support certificate revocation.');
}
if (null === $revocationReason) {
$revocationReason = RevocationReason::createDefaultReason();
}
openssl_x509_export(openssl_x509_read($certificate->getPEM()), $formattedPem);
$formattedPem = str_ireplace('-----BEGIN CERTIFICATE-----', '', $formattedPem);
$formattedPem = str_ireplace('-----END CERTIFICATE-----', '', $formattedPem);
$client = $this->getHttpClient();
$formattedPem = $client->getBase64Encoder()->encode(base64_decode(trim($formattedPem)));
try {
$client->request(
'POST',
$endpoint,
$client->signKidPayload($endpoint, $this->getResourceAccount(), ['certificate' => $formattedPem, 'reason' => $revocationReason->getReasonType()]),
false
);
} catch (AcmeCoreServerException $e) {
throw new CertificateRevocationException($e->getMessage(), $e);
} catch (AcmeCoreClientException $e) {
throw new CertificateRevocationException($e->getMessage(), $e);
}
}
/**
* Find a resource URL from the Certificate Authority.
*/
public function getResourceUrl(string $resource): string
{
if (!$this->directory) {
$this->directory = new ResourcesDirectory(
$this->getHttpClient()->request('GET', $this->directoryUrl)
);
}
return $this->directory->getResourceUrl($resource);
}
/**
* Request a resource (URL is found using ACME server directory).
*
* @throws AcmeCoreServerException when the ACME server returns an error HTTP status code
* @throws AcmeCoreClientException when an error occured during response parsing
*
* @return array|string
*/
protected function requestResource(string $method, string $resource, array $payload, bool $returnJson = true)
{
$client = $this->getHttpClient();
$endpoint = $this->getResourceUrl($resource);
return $client->request(
$method,
$endpoint,
$client->signJwkPayload($endpoint, $payload),
$returnJson
);
}
private function createCertificateResponse(CertificateRequest $csr, string $certificate): CertificateResponse
{
$certificateHeader = '-----BEGIN CERTIFICATE-----';
$certificatesChain = null;
foreach (array_reverse(explode($certificateHeader, $certificate)) as $pem) {
if ('' !== \trim($pem)) {
$certificatesChain = new Certificate($certificateHeader.$pem, $certificatesChain);
}
}
return new CertificateResponse($csr, $certificatesChain);
}
private function getResourceAccount(): string
{
if (!$this->account) {
$payload = [
'onlyReturnExisting' => true,
];
$this->requestResource('POST', ResourcesDirectory::NEW_ACCOUNT, $payload);
$this->account = $this->getHttpClient()->getLastLocation();
}
return $this->account;
}
private function createAuthorizationChallenge($domain, array $response): AuthorizationChallenge
{
$base64encoder = $this->getHttpClient()->getBase64Encoder();
return new AuthorizationChallenge(
$domain,
$response['status'],
$response['type'],
$response['url'],
$response['token'],
$response['token'].'.'.$base64encoder->encode($this->getHttpClient()->getJWKThumbprint())
);
}
private function getHttpClient(): SecureHttpClient
{
if (!$this->initializedHttpClient) {
$this->initializedHttpClient = $this->uninitializedHttpClient;
$this->initializedHttpClient->setNonceEndpoint($this->getResourceUrl(ResourcesDirectory::NEW_NONCE));
}
return $this->initializedHttpClient;
}
}

View File

@ -0,0 +1,187 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core;
use AcmePhp\Core\Exception\AcmeCoreClientException;
use AcmePhp\Core\Exception\AcmeCoreServerException;
use AcmePhp\Core\Exception\Protocol\CertificateRequestFailedException;
use AcmePhp\Core\Exception\Protocol\CertificateRequestTimedOutException;
use AcmePhp\Core\Exception\Protocol\CertificateRevocationException;
use AcmePhp\Core\Exception\Protocol\ChallengeFailedException;
use AcmePhp\Core\Exception\Protocol\ChallengeNotSupportedException;
use AcmePhp\Core\Exception\Protocol\ChallengeTimedOutException;
use AcmePhp\Core\Protocol\AuthorizationChallenge;
use AcmePhp\Core\Protocol\CertificateOrder;
use AcmePhp\Core\Protocol\ExternalAccount;
use AcmePhp\Core\Protocol\RevocationReason;
use AcmePhp\Ssl\Certificate;
use AcmePhp\Ssl\CertificateRequest;
use AcmePhp\Ssl\CertificateResponse;
/**
* ACME protocol client interface.
*
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
interface AcmeClientInterface
{
/**
* Register the local account KeyPair in the Certificate Authority.
*
* @param string|null $email an optionnal e-mail to associate with the account
* @param ExternalAccount|null $externalAccount an optionnal External Account to use for External Account Binding
*
* @throws AcmeCoreServerException when the ACME server returns an error HTTP status code
* (the exception will be more specific if detail is provided)
* @throws AcmeCoreClientException when an error occured during response parsing
*
* @return array the Certificate Authority response decoded from JSON into an array
*/
public function registerAccount(string $email = null, ExternalAccount $externalAccount = null): array;
/**
* Request authorization challenge data for a list of domains.
*
* An AuthorizationChallenge is an association between a URI, a token and a payload.
* The Certificate Authority will create this challenge data and you will then have
* to expose the payload for the verification (see challengeAuthorization).
*
* @param string[] $domains the domains to challenge
*
* @throws AcmeCoreServerException when the ACME server returns an error HTTP status code
* (the exception will be more specific if detail is provided)
* @throws AcmeCoreClientException when an error occured during response parsing
* @throws ChallengeNotSupportedException when the HTTP challenge is not supported by the server
*
* @return CertificateOrder the Order returned by the Certificate Authority
*/
public function requestOrder(array $domains): CertificateOrder;
/**
* Request the current status of a certificate order.
*/
public function reloadOrder(CertificateOrder $order): CertificateOrder;
/**
* Request a certificate for the given domain.
*
* This method should be called only if a previous authorization challenge has
* been successful for the asked domain.
*
* WARNING : This method SHOULD NOT BE USED in a web action. It will
* wait for the Certificate Authority to validate the certificate and
* this operation could be long.
*
* @param CertificateOrder $order the Order returned by the Certificate Authority
* @param CertificateRequest $csr the Certificate Signing Request (informations for the certificate)
* @param int $timeout the timeout period
* @param bool $returnAlternateCertificateIfAvailable whether the alternate certificate provided by
* the CA should be returned instead of the main one.
* This is especially useful following
* https://letsencrypt.org/2019/04/15/transitioning-to-isrg-root.html.
*
* @throws AcmeCoreServerException when the ACME server returns an error HTTP status code
* (the exception will be more specific if detail is provided)
* @throws AcmeCoreClientException when an error occured during response parsing
* @throws CertificateRequestFailedException when the certificate request failed
* @throws CertificateRequestTimedOutException when the certificate request timed out
*
* @return CertificateResponse the certificate data to save it somewhere you want
*/
public function finalizeOrder(CertificateOrder $order, CertificateRequest $csr, int $timeout = 180, bool $returnAlternateCertificateIfAvailable = false): CertificateResponse;
/**
* Request authorization challenge data for a given domain.
*
* An AuthorizationChallenge is an association between a URI, a token and a payload.
* The Certificate Authority will create this challenge data and you will then have
* to expose the payload for the verification (see challengeAuthorization).
*
* @param string $domain the domain to challenge
*
* @throws AcmeCoreServerException when the ACME server returns an error HTTP status code
* (the exception will be more specific if detail is provided)
* @throws AcmeCoreClientException when an error occured during response parsing
* @throws ChallengeNotSupportedException when the HTTP challenge is not supported by the server
*
* @return AuthorizationChallenge[] the list of challenges data returned by the Certificate Authority
*/
public function requestAuthorization(string $domain): array;
/**
* Request the current status of an authorization challenge.
*
* @param AuthorizationChallenge $challenge The challenge to request
*
* @return AuthorizationChallenge A new instance of the challenge
*/
public function reloadAuthorization(AuthorizationChallenge $challenge): AuthorizationChallenge;
/**
* Ask the Certificate Authority to challenge a given authorization.
*
* This check will generally consists of requesting over HTTP the domain
* at a specific URL. This URL should return the raw payload generated
* by requestAuthorization.
*
* WARNING : This method SHOULD NOT BE USED in a web action. It will
* wait for the Certificate Authority to validate the challenge and this
* operation could be long.
*
* @param AuthorizationChallenge $challenge the challenge data to check
* @param int $timeout the timeout period
*
* @throws AcmeCoreServerException when the ACME server returns an error HTTP status code
* (the exception will be more specific if detail is provided)
* @throws AcmeCoreClientException when an error occured during response parsing
* @throws ChallengeTimedOutException when the challenge timed out
* @throws ChallengeFailedException when the challenge failed
*
* @return array the validate challenge response
*/
public function challengeAuthorization(AuthorizationChallenge $challenge, int $timeout = 180): array;
/**
* Request a certificate for the given domain.
*
* This method should be called only if a previous authorization challenge has
* been successful for the asked domain.
*
* WARNING : This method SHOULD NOT BE USED in a web action. It will
* wait for the Certificate Authority to validate the certificate and
* this operation could be long.
*
* @param string $domain the domain to request a certificate for
* @param CertificateRequest $csr the Certificate Signing Request (informations for the certificate)
* @param int $timeout the timeout period
* @param bool $returnAlternateCertificateIfAvailable whether the alternate certificate provided by
* the CA should be returned instead of the main one.
* This is especially useful following
* https://letsencrypt.org/2019/04/15/transitioning-to-isrg-root.html.
*
* @throws AcmeCoreServerException when the ACME server returns an error HTTP status code
* (the exception will be more specific if detail is provided)
* @throws AcmeCoreClientException when an error occured during response parsing
* @throws CertificateRequestFailedException when the certificate request failed
* @throws CertificateRequestTimedOutException when the certificate request timed out
*
* @return CertificateResponse the certificate data to save it somewhere you want
*/
public function requestCertificate(string $domain, CertificateRequest $csr, int $timeout = 180, bool $returnAlternateCertificateIfAvailable = false): CertificateResponse;
/**
* Revoke a given certificate from the Certificate Authority.
*
* @throws CertificateRevocationException
*/
public function revokeCertificate(Certificate $certificate, RevocationReason $revocationReason = null);
}

View File

@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Challenge;
use AcmePhp\Core\Exception\Protocol\ChallengeNotSupportedException;
use AcmePhp\Core\Protocol\AuthorizationChallenge;
/**
* A strategy ACME validator.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class ChainValidator implements ValidatorInterface
{
/** @var ValidatorInterface[] */
private $validators;
/**
* @param ValidatorInterface[] $validators
*/
public function __construct(array $validators)
{
$this->validators = $validators;
}
/**
* {@inheritdoc}
*/
public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
{
foreach ($this->validators as $validator) {
if ($validator->supports($authorizationChallenge, $solver)) {
return true;
}
}
return false;
}
/**
* {@inheritdoc}
*/
public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
{
foreach ($this->validators as $validator) {
if ($validator->supports($authorizationChallenge, $solver)) {
return $validator->isValid($authorizationChallenge, $solver);
}
}
throw new ChallengeNotSupportedException();
}
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Challenge;
/**
* ACME challenge solver.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
interface ConfigurableServiceInterface
{
/**
* Configure the service with a set of configuration.
*/
public function configure(array $config);
}

View File

@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Challenge\Dns;
use AcmePhp\Core\Http\Base64SafeEncoder;
use AcmePhp\Core\Protocol\AuthorizationChallenge;
/**
* Extract data needed to solve DNS challenges.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class DnsDataExtractor
{
/** @var Base64SafeEncoder */
private $encoder;
public function __construct(Base64SafeEncoder $encoder = null)
{
$this->encoder = $encoder ?: new Base64SafeEncoder();
}
/**
* Retrieves the name of the TXT record to register.
*/
public function getRecordName(AuthorizationChallenge $authorizationChallenge): string
{
return sprintf('_acme-challenge.%s.', $authorizationChallenge->getDomain());
}
/**
* Retrieves the value of the TXT record to register.
*/
public function getRecordValue(AuthorizationChallenge $authorizationChallenge): string
{
return $this->encoder->encode(hash('sha256', $authorizationChallenge->getPayload(), true));
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Challenge\Dns;
/**
* Resolves DNS.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
interface DnsResolverInterface
{
/**
* Return whether or not the Resolver is supported.
*/
public static function isSupported(): bool;
/**
* Retrieves the list of TXT entries for the given domain.
*/
public function getTxtEntries(string $domain): array;
}

View File

@ -0,0 +1,68 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Challenge\Dns;
use AcmePhp\Core\Challenge\SolverInterface;
use AcmePhp\Core\Challenge\ValidatorInterface;
use AcmePhp\Core\Exception\AcmeDnsResolutionException;
use AcmePhp\Core\Protocol\AuthorizationChallenge;
/**
* Validator for DNS challenges.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class DnsValidator implements ValidatorInterface
{
/**
* @var DnsDataExtractor
*/
private $extractor;
/**
* @var DnsResolverInterface
*/
private $dnsResolver;
public function __construct(DnsDataExtractor $extractor = null, DnsResolverInterface $dnsResolver = null)
{
$this->extractor = $extractor ?: new DnsDataExtractor();
$this->dnsResolver = $dnsResolver;
if (!$this->dnsResolver) {
$this->dnsResolver = LibDnsResolver::isSupported() ? new LibDnsResolver() : new SimpleDnsResolver();
}
}
/**
* {@inheritdoc}
*/
public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
{
return 'dns-01' === $authorizationChallenge->getType();
}
/**
* {@inheritdoc}
*/
public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
{
$recordName = $this->extractor->getRecordName($authorizationChallenge);
$recordValue = $this->extractor->getRecordValue($authorizationChallenge);
try {
return \in_array($recordValue, $this->dnsResolver->getTxtEntries($recordName), false);
} catch (AcmeDnsResolutionException $e) {
return false;
}
}
}

View File

@ -0,0 +1,152 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Challenge\Dns;
use AcmePhp\Core\Challenge\ConfigurableServiceInterface;
use AcmePhp\Core\Challenge\MultipleChallengesSolverInterface;
use AcmePhp\Core\Protocol\AuthorizationChallenge;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use Webmozart\Assert\Assert;
/**
* ACME DNS solver with automate configuration of a Gandi.Net.
*
* @author Alexander Obuhovich <aik.bold@gmail.com>
*/
class GandiSolver implements MultipleChallengesSolverInterface, ConfigurableServiceInterface
{
use LoggerAwareTrait;
/**
* @var DnsDataExtractor
*/
private $extractor;
/**
* @var ClientInterface
*/
private $client;
/**
* @var array
*/
private $cacheZones;
/**
* @var string
*/
private $apiKey;
public function __construct(DnsDataExtractor $extractor = null, ClientInterface $client = null)
{
$this->extractor = $extractor ?: new DnsDataExtractor();
$this->client = $client ?: new Client();
$this->logger = new NullLogger();
}
/**
* Configure the service with a set of configuration.
*/
public function configure(array $config)
{
$this->apiKey = $config['api_key'];
}
/**
* {@inheritdoc}
*/
public function supports(AuthorizationChallenge $authorizationChallenge): bool
{
return 'dns-01' === $authorizationChallenge->getType();
}
/**
* {@inheritdoc}
*/
public function solve(AuthorizationChallenge $authorizationChallenge)
{
return $this->solveAll([$authorizationChallenge]);
}
/**
* {@inheritdoc}
*/
public function solveAll(array $authorizationChallenges)
{
Assert::allIsInstanceOf($authorizationChallenges, AuthorizationChallenge::class);
foreach ($authorizationChallenges as $authorizationChallenge) {
$topLevelDomain = $this->getTopLevelDomain($authorizationChallenge->getDomain());
$recordName = $this->extractor->getRecordName($authorizationChallenge);
$recordValue = $this->extractor->getRecordValue($authorizationChallenge);
$subDomain = \str_replace('.'.$topLevelDomain.'.', '', $recordName);
$this->client->request(
'PUT',
'https://dns.api.gandi.net/api/v5/domains/'.$topLevelDomain.'/records/'.$subDomain.'/TXT',
[
'headers' => [
'X-Api-Key' => $this->apiKey,
],
'json' => [
'rrset_type' => 'TXT',
'rrset_ttl' => 600,
'rrset_name' => $subDomain,
'rrset_values' => [$recordValue],
],
]
);
}
}
/**
* {@inheritdoc}
*/
public function cleanup(AuthorizationChallenge $authorizationChallenge)
{
return $this->cleanupAll([$authorizationChallenge]);
}
/**
* {@inheritdoc}
*/
public function cleanupAll(array $authorizationChallenges)
{
Assert::allIsInstanceOf($authorizationChallenges, AuthorizationChallenge::class);
foreach ($authorizationChallenges as $authorizationChallenge) {
$topLevelDomain = $this->getTopLevelDomain($authorizationChallenge->getDomain());
$recordName = $this->extractor->getRecordName($authorizationChallenge);
$subDomain = \str_replace('.'.$topLevelDomain.'.', '', $recordName);
$this->client->request(
'DELETE',
'https://dns.api.gandi.net/api/v5/domains/'.$topLevelDomain.'/records/'.$subDomain.'/TXT',
[
'headers' => [
'X-Api-Key' => $this->apiKey,
],
]
);
}
}
protected function getTopLevelDomain(string $domain): string
{
return \implode('.', \array_slice(\explode('.', $domain), -2));
}
}

View File

@ -0,0 +1,200 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Challenge\Dns;
use AcmePhp\Core\Exception\AcmeDnsResolutionException;
use LibDNS\Decoder\Decoder;
use LibDNS\Decoder\DecoderFactory;
use LibDNS\Encoder\Encoder;
use LibDNS\Encoder\EncoderFactory;
use LibDNS\Messages\MessageFactory;
use LibDNS\Messages\MessageTypes;
use LibDNS\Records\QuestionFactory;
use LibDNS\Records\ResourceTypes;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
/**
* Resolves DNS with LibDNS (pass over internal DNS cache and check several nameservers).
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class LibDnsResolver implements DnsResolverInterface
{
use LoggerAwareTrait;
/**
* @var QuestionFactory
*/
private $questionFactory;
/**
* @var MessageFactory
*/
private $messageFactory;
/**
* @var Encoder
*/
private $encoder;
/**
* @var Decoder
*/
private $decoder;
/**
* @var string
*/
private $nameServer;
public function __construct(
QuestionFactory $questionFactory = null,
MessageFactory $messageFactory = null,
Encoder $encoder = null,
Decoder $decoder = null,
$nameServer = '8.8.8.8'
) {
$this->questionFactory = $questionFactory ?: new QuestionFactory();
$this->messageFactory = $messageFactory ?: new MessageFactory();
$this->encoder = $encoder ?: (new EncoderFactory())->create();
$this->decoder = $decoder ?: (new DecoderFactory())->create();
$this->nameServer = $nameServer;
$this->logger = new NullLogger();
}
/**
* @{@inheritdoc}
*/
public static function isSupported(): bool
{
return class_exists(ResourceTypes::class);
}
/**
* @{@inheritdoc}
*/
public function getTxtEntries($domain): array
{
$domain = rtrim($domain, '.');
$nameServers = $this->getNameServers($domain);
$this->logger->debug('Fetched TXT records for domain', ['nsDomain' => $domain, 'servers' => $nameServers]);
$identicalEntries = [];
foreach ($nameServers as $nameServer) {
$ipNameServer = gethostbynamel($nameServer);
if (empty($ipNameServer)) {
throw new AcmeDnsResolutionException(sprintf('Unable to find domain %s on nameserver %s', $domain, $nameServer));
}
try {
$response = $this->request($domain, ResourceTypes::TXT, $ipNameServer[0]);
} catch (\Exception $e) {
throw new AcmeDnsResolutionException(sprintf('Unable to find domain %s on nameserver %s', $domain, $nameServer));
}
$entries = [];
foreach ($response->getAnswerRecords() as $record) {
foreach ($record->getData() as $recordData) {
$entries[] = (string) $recordData;
}
}
$identicalEntries[json_encode($entries)][] = $nameServer;
}
$this->logger->info('DNS records fetched', ['mapping' => $identicalEntries]);
if (1 !== \count($identicalEntries)) {
throw new AcmeDnsResolutionException('Dns not fully propagated');
}
return json_decode(key($identicalEntries));
}
private function getNameServers($domain)
{
if ('' === $domain) {
return [$this->nameServer];
}
$parentNameServers = $this->getNameServers(implode('.', \array_slice(explode('.', $domain), 1)));
$itemNameServers = [];
$this->logger->debug('Fetched NS in charge of domain', ['nsDomain' => $domain, 'servers' => $parentNameServers]);
foreach ($parentNameServers as $parentNameServer) {
$ipNameServer = gethostbynamel($parentNameServer);
if (empty($ipNameServer)) {
continue;
}
try {
$response = $this->request(
$domain,
ResourceTypes::NS,
$ipNameServer[0]
);
} catch (\Exception $e) {
// ignore errors
continue;
}
foreach ($response->getAnswerRecords() as $record) {
try {
$field = $record->getData()->getFieldByName('nsdname');
$itemNameServers[] = $field->getValue();
} catch (\OutOfBoundsException $e) {
}
}
foreach ($response->getAuthorityRecords() as $record) {
try {
$field = $record->getData()->getFieldByName('nsdname');
$itemNameServers[] = $field->getValue();
} catch (\OutOfBoundsException $e) {
}
}
}
$itemNameServers = array_unique($itemNameServers);
if (empty($itemNameServers)) {
return $parentNameServers;
}
return $itemNameServers;
}
private function request($domain, $type, $nameServer)
{
$question = $this->questionFactory->create($type);
$question->setName($domain);
// Create request message
$request = $this->messageFactory->create(MessageTypes::QUERY);
$request->getQuestionRecords()->add($question);
$request->isRecursionDesired(true);
// Send request
$socket = stream_socket_client(sprintf('udp://'.$nameServer.':53'));
stream_socket_sendto($socket, $this->encoder->encode($request));
$r = [$socket];
$w = $e = [];
if (!stream_select($r, $w, $e, 3)) {
throw new AcmeDnsResolutionException(sprintf('Timeout reached when requesting ServerName %s', $nameServer));
}
// Decode response message
try {
$response = $this->decoder->decode(fread($socket, 1 << 20));
} catch (\Exception $e) {
throw new AcmeDnsResolutionException('Failed to decode server response', $e);
}
if (0 !== $response->getResponseCode()) {
throw new AcmeDnsResolutionException(sprintf('ServerName respond with error code "%d"', $response->getResponseCode()));
}
return $response;
}
}

View File

@ -0,0 +1,295 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Challenge\Dns;
use AcmePhp\Core\Challenge\MultipleChallengesSolverInterface;
use AcmePhp\Core\Exception\Protocol\ChallengeFailedException;
use AcmePhp\Core\Protocol\AuthorizationChallenge;
use Aws\Route53\Route53Client;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use Webmozart\Assert\Assert;
/**
* ACME DNS solver with automate configuration of a AWS route53.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class Route53Solver implements MultipleChallengesSolverInterface
{
use LoggerAwareTrait;
/**
* @var DnsDataExtractor
*/
private $extractor;
/**
* @var Route53Client
*/
private $client;
/**
* @var array
*/
private $cacheZones;
public function __construct(DnsDataExtractor $extractor = null, Route53Client $client = null)
{
$this->extractor = $extractor ?: new DnsDataExtractor();
$this->client = $client ?: new Route53Client([]);
$this->logger = new NullLogger();
}
/**
* {@inheritdoc}
*/
public function supports(AuthorizationChallenge $authorizationChallenge): bool
{
return 'dns-01' === $authorizationChallenge->getType();
}
/**
* {@inheritdoc}
*/
public function solve(AuthorizationChallenge $authorizationChallenge)
{
return $this->solveAll([$authorizationChallenge]);
}
/**
* {@inheritdoc}
*/
public function solveAll(array $authorizationChallenges)
{
Assert::allIsInstanceOf($authorizationChallenges, AuthorizationChallenge::class);
$changesPerZone = [];
$authorizationChallengesPerDomain = $this->groupAuthorizationChallengesPerDomain($authorizationChallenges);
foreach ($authorizationChallengesPerDomain as $domain => $authorizationChallengesForDomain) {
$zone = $this->getZone($authorizationChallengesForDomain[0]->getDomain());
$authorizationChallengesPerRecordName = $this->groupAuthorizationChallengesPerRecordName($authorizationChallengesForDomain);
foreach ($authorizationChallengesPerRecordName as $recordName => $authorizationChallengesForRecordName) {
$challengeValues = array_unique(array_map([$this->extractor, 'getRecordValue'], $authorizationChallengesForRecordName));
$recordIndex = $this->getPreviousRecordIndex($zone['Id'], $recordName);
if (0 === \count(array_diff($challengeValues, array_keys($recordIndex)))) {
$this->logger->debug('Record already defined', ['recordName' => $recordName]);
continue;
}
foreach ($challengeValues as $recordValue) {
$recordIndex[$recordValue] = time();
}
$changesPerZone[$zone['Id']][] = $this->getSaveRecordQuery($recordName, $recordIndex);
}
}
$records = [];
foreach ($changesPerZone as $zoneId => $changes) {
$this->logger->info('Updating route 53 DNS', ['zone' => $zoneId]);
$records[$zoneId] = $this->client->changeResourceRecordSets(
[
'ChangeBatch' => [
'Changes' => $changes,
],
'HostedZoneId' => $zoneId,
]
);
}
foreach ($records as $zoneId => $record) {
$this->logger->info('Waiting for Route 53 changes', ['zone' => $zoneId]);
$this->client->waitUntil('ResourceRecordSetsChanged', ['Id' => $record['ChangeInfo']['Id']]);
}
}
/**
* {@inheritdoc}
*/
public function cleanup(AuthorizationChallenge $authorizationChallenge)
{
return $this->cleanupAll([$authorizationChallenge]);
}
/**
* {@inheritdoc}
*/
public function cleanupAll(array $authorizationChallenges)
{
Assert::allIsInstanceOf($authorizationChallenges, AuthorizationChallenge::class);
$changesPerZone = [];
$authorizationChallengesPerDomain = $this->groupAuthorizationChallengesPerDomain($authorizationChallenges);
foreach ($authorizationChallengesPerDomain as $domain => $authorizationChallengesForDomain) {
$zone = $this->getZone($authorizationChallengesForDomain[0]->getDomain());
$authorizationChallengesPerRecordName = $this->groupAuthorizationChallengesPerRecordName($authorizationChallengesForDomain);
foreach ($authorizationChallengesPerRecordName as $recordName => $authorizationChallengesForRecordName) {
$challengeValues = array_unique(array_map([$this->extractor, 'getRecordValue'], $authorizationChallengesForRecordName));
$recordIndex = $this->getPreviousRecordIndex($zone['Id'], $recordName);
foreach ($challengeValues as $recordValue) {
unset($recordIndex[$recordValue]);
}
$changesPerZone[$zone['Id']][] = $this->getSaveRecordQuery($recordName, $recordIndex);
}
}
foreach ($changesPerZone as $zoneId => $changes) {
$this->logger->info('Updating route 53 DNS', ['zone' => $zoneId]);
$this->client->changeResourceRecordSets(
[
'ChangeBatch' => [
'Changes' => $changes,
],
'HostedZoneId' => $zoneId,
]
);
}
}
private function getPreviousRecordIndex($zoneId, $recordName)
{
$previousRecordSets = $this->client->listResourceRecordSets([
'HostedZoneId' => $zoneId,
'StartRecordName' => $recordName,
'StartRecordType' => 'TXT',
]);
$recordSets = array_filter(
$previousRecordSets['ResourceRecordSets'],
function ($recordSet) use ($recordName) {
return $recordSet['Name'] === $recordName && 'TXT' === $recordSet['Type'];
}
);
$recordIndex = [];
foreach ($recordSets as $previousRecordSet) {
$previousTxt = array_map(function ($resourceRecord) {
return stripslashes(trim($resourceRecord['Value'], '"'));
}, $previousRecordSet['ResourceRecords']);
// Search the special Index
foreach ($previousTxt as $index => $recordValue) {
if (null !== $previousIndex = json_decode($recordValue, true)) {
$recordIndex = $previousIndex;
unset($previousTxt[$index]);
break;
}
}
// Set default value
foreach ($previousTxt as $recordValue) {
if (!isset($recordIndex[$recordValue])) {
$recordIndex[$recordValue] = time();
}
}
}
return $recordIndex;
}
private function getSaveRecordQuery($recordName, array $recordIndex)
{
//remove old indexes
$limitTime = time() - 86400;
foreach ($recordIndex as $recordValue => $time) {
if ($time < $limitTime) {
unset($recordIndex[$recordValue]);
}
}
$recordValues = array_keys($recordIndex);
$recordValues[] = json_encode($recordIndex);
return [
'Action' => 'UPSERT',
'ResourceRecordSet' => [
'Name' => $recordName,
'ResourceRecords' => array_map(function ($recordValue) {
return [
'Value' => sprintf('"%s"', addslashes($recordValue)),
];
}, $recordValues),
'TTL' => 5,
'Type' => 'TXT',
],
];
}
/**
* @param AuthorizationChallenge[] $authorizationChallenges
*
* @return AuthorizationChallenge[][]
*/
private function groupAuthorizationChallengesPerDomain(array $authorizationChallenges)
{
$groups = [];
foreach ($authorizationChallenges as $authorizationChallenge) {
$groups[$authorizationChallenge->getDomain()][] = $authorizationChallenge;
}
return $groups;
}
/**
* @param AuthorizationChallenge[] $authorizationChallenges
*
* @return AuthorizationChallenge[][]
*/
private function groupAuthorizationChallengesPerRecordName(array $authorizationChallenges)
{
$groups = [];
foreach ($authorizationChallenges as $authorizationChallenge) {
$groups[$this->extractor->getRecordName($authorizationChallenge)][] = $authorizationChallenge;
}
return $groups;
}
private function getZone($domain)
{
$domainParts = explode('.', $domain);
$domains = array_reverse(array_map(
function ($index) use ($domainParts) {
return implode('.', \array_slice($domainParts, \count($domainParts) - $index));
},
range(0, \count($domainParts))
));
$zones = $this->getZones();
foreach ($domains as $cursorDomain) {
if (isset($zones[$cursorDomain.'.'])) {
return $zones[$cursorDomain.'.'];
}
}
throw new ChallengeFailedException(sprintf('Unable to find a zone for the domain "%s"', $domain));
}
private function getZones()
{
if (null !== $this->cacheZones) {
return $this->cacheZones;
}
$zones = [];
$args = [];
do {
$resp = $this->client->listHostedZones($args);
$zones = array_merge($zones, $resp['HostedZones']);
$args = ['Marker' => $resp['NextMarker']];
} while ($resp['IsTruncated']);
$this->cacheZones = array_column($zones, null, 'Name');
return $this->cacheZones;
}
}

View File

@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Challenge\Dns;
/**
* Resolves DNS through dns_get_record.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class SimpleDnsResolver implements DnsResolverInterface
{
/**
* @{@inheritdoc}
*/
public static function isSupported(): bool
{
return \function_exists('dns_get_record');
}
/**
* @{@inheritdoc}
*/
public function getTxtEntries($domain): array
{
$entries = [];
foreach (dns_get_record($domain, DNS_TXT) as $record) {
$entries = array_merge($entries, $record['entries']);
}
sort($entries);
return array_unique($entries);
}
}

View File

@ -0,0 +1,100 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Challenge\Dns;
use AcmePhp\Core\Challenge\SolverInterface;
use AcmePhp\Core\Protocol\AuthorizationChallenge;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
/**
* ACME DNS solver with manual intervention.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class SimpleDnsSolver implements SolverInterface
{
/**
* @var DnsDataExtractor
*/
private $extractor;
/**
* @var OutputInterface
*/
protected $output;
/**
* @param DnsDataExtractor $extractor
* @param OutputInterface $output
*/
public function __construct(DnsDataExtractor $extractor = null, OutputInterface $output = null)
{
$this->extractor = $extractor ?: new DnsDataExtractor();
$this->output = $output ?: new NullOutput();
}
/**
* {@inheritdoc}
*/
public function supports(AuthorizationChallenge $authorizationChallenge): bool
{
return 'dns-01' === $authorizationChallenge->getType();
}
/**
* {@inheritdoc}
*/
public function solve(AuthorizationChallenge $authorizationChallenge)
{
$recordName = $this->extractor->getRecordName($authorizationChallenge);
$recordValue = $this->extractor->getRecordValue($authorizationChallenge);
$this->output->writeln(
sprintf(
<<<'EOF'
Add the following TXT record to your DNS zone
Domain: %s
TXT value: %s
<comment>Wait for the propagation before moving to the next step</comment>
Tips: Use the following command to check the propagation
host -t TXT %s
EOF
,
$recordName,
$recordValue,
$recordName
)
);
}
/**
* {@inheritdoc}
*/
public function cleanup(AuthorizationChallenge $authorizationChallenge)
{
$recordName = $this->extractor->getRecordName($authorizationChallenge);
$this->output->writeln(
sprintf(
<<<'EOF'
You can now cleanup your DNS by removing the domain <comment>_acme-challenge.%s.</comment>
EOF
,
$recordName
)
);
}
}

View File

@ -0,0 +1,90 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Challenge\Http;
use AcmePhp\Core\Challenge\ConfigurableServiceInterface;
use AcmePhp\Core\Challenge\SolverInterface;
use AcmePhp\Core\Filesystem\Adapter\NullAdapter;
use AcmePhp\Core\Filesystem\FilesystemFactoryInterface;
use AcmePhp\Core\Filesystem\FilesystemInterface;
use AcmePhp\Core\Protocol\AuthorizationChallenge;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Webmozart\Assert\Assert;
/**
* ACME HTTP solver through ftp upload.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class FilesystemSolver implements SolverInterface, ConfigurableServiceInterface
{
/**
* @var ContainerInterface
*/
private $filesystemFactoryLocator;
/**
* @var FilesystemInterface
*/
private $filesystem;
/**
* @var HttpDataExtractor
*/
private $extractor;
public function __construct(ContainerInterface $filesystemFactoryLocator = null, HttpDataExtractor $extractor = null)
{
$this->filesystemFactoryLocator = $filesystemFactoryLocator ?: new ServiceLocator([]);
$this->extractor = $extractor ?: new HttpDataExtractor();
$this->filesystem = new NullAdapter();
}
public function configure(array $config)
{
Assert::keyExists($config, 'adapter', 'configure::$config expected an array with the key %s.');
/** @var FilesystemFactoryInterface $factory */
$factory = $this->filesystemFactoryLocator->get($config['adapter']);
$this->filesystem = $factory->create($config);
}
/**
* {@inheritdoc}
*/
public function supports(AuthorizationChallenge $authorizationChallenge): bool
{
return 'http-01' === $authorizationChallenge->getType();
}
/**
* {@inheritdoc}
*/
public function solve(AuthorizationChallenge $authorizationChallenge)
{
$checkPath = $this->extractor->getCheckPath($authorizationChallenge);
$checkContent = $this->extractor->getCheckContent($authorizationChallenge);
$this->filesystem->write($checkPath, $checkContent);
}
/**
* {@inheritdoc}
*/
public function cleanup(AuthorizationChallenge $authorizationChallenge)
{
$checkPath = $this->extractor->getCheckPath($authorizationChallenge);
$this->filesystem->delete($checkPath);
}
}

View File

@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Challenge\Http;
use AcmePhp\Core\Protocol\AuthorizationChallenge;
/**
* Extract data needed to solve HTTP challenges.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class HttpDataExtractor
{
/**
* Retrieves the absolute URL called by the CA.
*/
public function getCheckUrl(AuthorizationChallenge $authorizationChallenge): string
{
return sprintf(
'http://%s%s',
$authorizationChallenge->getDomain(),
$this->getCheckPath($authorizationChallenge)
);
}
/**
* Retrieves the absolute path called by the CA.
*/
public function getCheckPath(AuthorizationChallenge $authorizationChallenge): string
{
return sprintf(
'/.well-known/acme-challenge/%s',
$authorizationChallenge->getToken()
);
}
/**
* Retrieves the content that should be returned in the response.
*/
public function getCheckContent(AuthorizationChallenge $authorizationChallenge): string
{
return $authorizationChallenge->getPayload();
}
}

View File

@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Challenge\Http;
use AcmePhp\Core\Challenge\SolverInterface;
use AcmePhp\Core\Challenge\ValidatorInterface;
use AcmePhp\Core\Protocol\AuthorizationChallenge;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
/**
* Validator for HTTP challenges.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class HttpValidator implements ValidatorInterface
{
/**
* @var HttpDataExtractor
*/
private $extractor;
/**
* @var Client
*/
private $client;
public function __construct(HttpDataExtractor $extractor = null, Client $client = null)
{
$this->extractor = $extractor ?: new HttpDataExtractor();
$this->client = $client ?: new Client();
}
/**
* {@inheritdoc}
*/
public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
{
return 'http-01' === $authorizationChallenge->getType() && !$solver instanceof MockServerHttpSolver;
}
/**
* {@inheritdoc}
*/
public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
{
$checkUrl = $this->extractor->getCheckUrl($authorizationChallenge);
$checkContent = $this->extractor->getCheckContent($authorizationChallenge);
try {
return $checkContent === trim($this->client->get($checkUrl, ['verify' => false])->getBody()->getContents());
} catch (ClientException $e) {
return false;
}
}
}

View File

@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Challenge\Http;
use AcmePhp\Core\Challenge\SolverInterface;
use AcmePhp\Core\Challenge\ValidatorInterface;
use AcmePhp\Core\Protocol\AuthorizationChallenge;
/**
* Validator for pebble-challtestsrv.
*
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class MockHttpValidator implements ValidatorInterface
{
/**
* {@inheritdoc}
*/
public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
{
return 'http-01' === $authorizationChallenge->getType() && $solver instanceof MockServerHttpSolver;
}
/**
* {@inheritdoc}
*/
public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
{
return true;
}
}

View File

@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Challenge\Http;
use AcmePhp\Core\Challenge\SolverInterface;
use AcmePhp\Core\Protocol\AuthorizationChallenge;
use GuzzleHttp\Client;
use GuzzleHttp\RequestOptions;
/**
* ACME HTTP solver talking to pebble-challtestsrv.
*
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class MockServerHttpSolver implements SolverInterface
{
/**
* {@inheritdoc}
*/
public function supports(AuthorizationChallenge $authorizationChallenge): bool
{
return 'http-01' === $authorizationChallenge->getType();
}
/**
* {@inheritdoc}
*/
public function solve(AuthorizationChallenge $authorizationChallenge)
{
(new Client())->post('http://localhost:8055/add-http01', [
RequestOptions::JSON => [
'token' => $authorizationChallenge->getToken(),
'content' => $authorizationChallenge->getPayload(),
],
]);
}
/**
* {@inheritdoc}
*/
public function cleanup(AuthorizationChallenge $authorizationChallenge)
{
(new Client())->post('http://localhost:8055/del-http01', [
RequestOptions::JSON => [
'token' => $authorizationChallenge->getToken(),
],
]);
}
}

View File

@ -0,0 +1,96 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Challenge\Http;
use AcmePhp\Core\Challenge\SolverInterface;
use AcmePhp\Core\Protocol\AuthorizationChallenge;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
/**
* ACME HTTP solver with manual intervention.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class SimpleHttpSolver implements SolverInterface
{
/**
* @var HttpDataExtractor
*/
private $extractor;
/**
* @var OutputInterface
*/
private $output;
public function __construct(HttpDataExtractor $extractor = null, OutputInterface $output = null)
{
$this->extractor = $extractor ?: new HttpDataExtractor();
$this->output = $output ?: new NullOutput();
}
/**
* {@inheritdoc}
*/
public function supports(AuthorizationChallenge $authorizationChallenge): bool
{
return 'http-01' === $authorizationChallenge->getType();
}
/**
* {@inheritdoc}
*/
public function solve(AuthorizationChallenge $authorizationChallenge)
{
$checkUrl = $this->extractor->getCheckUrl($authorizationChallenge);
$checkContent = $this->extractor->getCheckContent($authorizationChallenge);
$this->output->writeln(
sprintf(
<<<'EOF'
Create a text file accessible on URL %s
containing the following content:
%s
Check in your browser that the URL %s returns
the authorization token above.
EOF
,
$checkUrl,
$checkContent,
$checkContent
)
);
}
/**
* {@inheritdoc}
*/
public function cleanup(AuthorizationChallenge $authorizationChallenge)
{
$checkUrl = $this->extractor->getCheckUrl($authorizationChallenge);
$this->output->writeln(
sprintf(
<<<'EOF'
You can now safely remove the challenge's file at %s
EOF
,
$checkUrl
)
);
}
}

View File

@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Challenge;
use AcmePhp\Core\Protocol\AuthorizationChallenge;
/**
* ACME challenge solver able to solve several challenges at once.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
interface MultipleChallengesSolverInterface extends SolverInterface
{
/**
* Solve the given list of authorization challenge.
*
* @param AuthorizationChallenge[] $authorizationChallenges
*/
public function solveAll(array $authorizationChallenges);
/**
* Cleanup the environments after all challenges.
*
* @param AuthorizationChallenge[] $authorizationChallenges
*/
public function cleanupAll(array $authorizationChallenges);
}

View File

@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Challenge;
use AcmePhp\Core\Protocol\AuthorizationChallenge;
/**
* ACME challenge solver.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
interface SolverInterface
{
/**
* Determines whether or not the solver supports a given Challenge.
*
* @return bool The solver supports the given challenge's type
*/
public function supports(AuthorizationChallenge $authorizationChallenge): bool;
/**
* Solve the given authorization challenge.
*/
public function solve(AuthorizationChallenge $authorizationChallenge);
/**
* Cleanup the environments after a successful challenge.
*/
public function cleanup(AuthorizationChallenge $authorizationChallenge);
}

View File

@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Challenge;
use AcmePhp\Core\Protocol\AuthorizationChallenge;
/**
* ACME challenge pre validator.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
interface ValidatorInterface
{
/**
* Determines whether or not the validator supports a given Challenge.
*
* @return bool The validator supports the given challenge's type
*/
public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool;
/**
* Internally validate the challenge by performing the same kind of test than the CA.
*
* @return bool The challenge is valid
*/
public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool;
}

View File

@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Challenge;
use AcmePhp\Core\Protocol\AuthorizationChallenge;
/**
* ACME Challenge validator who implement a retry strategy till the decorated validator successfully validate the
* challenge or the timeout is reached.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class WaitingValidator implements ValidatorInterface
{
/** @var ValidatorInterface */
private $validator;
/** @var int */
private $timeout;
public function __construct(ValidatorInterface $validator, int $timeout = 180)
{
$this->validator = $validator;
$this->timeout = $timeout;
}
/**
* {@inheritdoc}
*/
public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
{
return $this->validator->supports($authorizationChallenge, $solver);
}
/**
* {@inheritdoc}
*/
public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
{
$limitEndTime = time() + $this->timeout;
do {
if ($this->validator->isValid($authorizationChallenge, $solver)) {
return true;
}
sleep(3);
} while ($limitEndTime > time());
return false;
}
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception;
/**
* Error reported by the client.
*
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class AcmeCoreClientException extends AcmeCoreException
{
public function __construct($message, \Exception $previous = null)
{
parent::__construct($message, 0, $previous);
}
}

View File

@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception;
/**
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class AcmeCoreException extends \RuntimeException
{
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception;
use Psr\Http\Message\RequestInterface;
/**
* Error reported by the server.
*
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class AcmeCoreServerException extends AcmeCoreException
{
public function __construct(RequestInterface $request, $message, \Exception $previous = null)
{
parent::__construct($message, $previous ? $previous->getCode() : 0, $previous);
}
}

View File

@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class AcmeDnsResolutionException extends AcmeCoreException
{
public function __construct($message, \Exception $previous = null)
{
parent::__construct(null === $message ? 'An exception was thrown during resolution of DNS' : $message, 0, $previous);
}
}

View File

@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Protocol;
/**
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class CertificateRequestFailedException extends ProtocolException
{
public function __construct(string $response)
{
parent::__construct(sprintf('Certificate request failed (response: %s)', $response));
}
}

View File

@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Protocol;
/**
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class CertificateRequestTimedOutException extends ProtocolException
{
public function __construct(string $response)
{
parent::__construct(sprintf('Certificate request timed out (response: %s)', $response));
}
}

View File

@ -0,0 +1,18 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Protocol;
use AcmePhp\Core\Exception\AcmeCoreClientException;
class CertificateRevocationException extends AcmeCoreClientException
{
}

View File

@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Protocol;
/**
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class ChallengeFailedException extends ProtocolException
{
private $response;
public function __construct($response, \Exception $previous = null)
{
parent::__construct(
sprintf('Challenge failed (response: %s).', json_encode($response)),
$previous
);
$this->response = $response;
}
/**
* @return array
*/
public function getResponse()
{
return $this->response;
}
}

View File

@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Protocol;
/**
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class ChallengeNotSupportedException extends ProtocolException
{
public function __construct(\Exception $previous = null)
{
parent::__construct('This ACME server does not expose supported challenge.', $previous);
}
}

View File

@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Protocol;
/**
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class ChallengeTimedOutException extends ProtocolException
{
private $response;
public function __construct($response, \Exception $previous = null)
{
parent::__construct(
sprintf('Challenge timed out (response: %s).', json_encode($response)),
$previous
);
$this->response = $response;
}
/**
* @return array
*/
public function getResponse()
{
return $this->response;
}
}

View File

@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Protocol;
/**
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class ExpectedJsonException extends ProtocolException
{
}

View File

@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Protocol;
use AcmePhp\Core\Exception\AcmeCoreClientException;
/**
* Error because the protocol was not respected.
*
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class ProtocolException extends AcmeCoreClientException
{
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Server;
use AcmePhp\Core\Exception\AcmeCoreServerException;
use Psr\Http\Message\RequestInterface;
/**
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class BadCsrServerException extends AcmeCoreServerException
{
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
{
parent::__construct(
$request,
'[badCSR] The CSR is unacceptable (e.g., due to a short key): '.$detail,
$previous
);
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Server;
use AcmePhp\Core\Exception\AcmeCoreServerException;
use Psr\Http\Message\RequestInterface;
/**
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class BadNonceServerException extends AcmeCoreServerException
{
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
{
parent::__construct(
$request,
'[badNonce] The client sent an unacceptable anti-replay nonce: '.$detail,
$previous
);
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Server;
use AcmePhp\Core\Exception\AcmeCoreServerException;
use Psr\Http\Message\RequestInterface;
/**
* @author Alex Plekhanov <alex@plekhanov.dev>
*/
class CaaServerException extends AcmeCoreServerException
{
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
{
parent::__construct(
$request,
'[caa] Certification Authority Authorization (CAA) records forbid the CA from issuing a certificate: '.$detail,
$previous
);
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Server;
use AcmePhp\Core\Exception\AcmeCoreServerException;
use Psr\Http\Message\RequestInterface;
/**
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class ConnectionServerException extends AcmeCoreServerException
{
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
{
parent::__construct(
$request,
'[connection] The server could not connect to the client for DV: '.$detail,
$previous
);
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Server;
use AcmePhp\Core\Exception\AcmeCoreServerException;
use Psr\Http\Message\RequestInterface;
/**
* @author Alex Plekhanov <alex@plekhanov.dev>
*/
class DnsServerException extends AcmeCoreServerException
{
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
{
parent::__construct(
$request,
'[dns] There was a problem with a DNS query during identifier validation: '.$detail,
$previous
);
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Server;
use AcmePhp\Core\Exception\AcmeCoreServerException;
use Psr\Http\Message\RequestInterface;
/**
* @author Alex Plekhanov <alex@plekhanov.dev>
*/
class IncorrectResponseServerException extends AcmeCoreServerException
{
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
{
parent::__construct(
$request,
"[incorrectResponse] Response received didnt match the challenge's requirements: ".$detail,
$previous
);
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Server;
use AcmePhp\Core\Exception\AcmeCoreServerException;
use Psr\Http\Message\RequestInterface;
/**
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class InternalServerException extends AcmeCoreServerException
{
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
{
parent::__construct(
$request,
'[serverInternal] The server experienced an internal error: '.$detail,
$previous
);
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Server;
use AcmePhp\Core\Exception\AcmeCoreServerException;
use Psr\Http\Message\RequestInterface;
/**
* @author Alex Plekhanov <alex@plekhanov.dev>
*/
class InvalidContactServerException extends AcmeCoreServerException
{
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
{
parent::__construct(
$request,
'[invalidContact] A contact URL for an account was invalid: '.$detail,
$previous
);
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Server;
use AcmePhp\Core\Exception\AcmeCoreServerException;
use Psr\Http\Message\RequestInterface;
/**
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class InvalidEmailServerException extends AcmeCoreServerException
{
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
{
parent::__construct(
$request,
'[invalidEmail] This email is unacceptable (e.g., it is invalid): '.$detail,
$previous
);
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Server;
use AcmePhp\Core\Exception\AcmeCoreServerException;
use Psr\Http\Message\RequestInterface;
/**
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class MalformedServerException extends AcmeCoreServerException
{
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
{
parent::__construct(
$request,
'[malformed] The request message was malformed: '.$detail,
$previous
);
}
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Server;
use AcmePhp\Core\Exception\AcmeCoreServerException;
use Psr\Http\Message\RequestInterface;
class OrderNotReadyServerException extends AcmeCoreServerException
{
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
{
parent::__construct(
$request,
'[orderNotReady] Order could not be finalized: '.$detail,
$previous
);
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Server;
use AcmePhp\Core\Exception\AcmeCoreServerException;
use Psr\Http\Message\RequestInterface;
/**
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class RateLimitedServerException extends AcmeCoreServerException
{
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
{
parent::__construct(
$request,
'[rateLimited] This client reached the rate limit of the server: '.$detail,
$previous
);
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Server;
use AcmePhp\Core\Exception\AcmeCoreServerException;
use Psr\Http\Message\RequestInterface;
/**
* @author Alex Plekhanov <alex@plekhanov.dev>
*/
class RejectedIdentifierServerException extends AcmeCoreServerException
{
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
{
parent::__construct(
$request,
'[rejectedIdentifier] The server will not issue certificates for the identifier: '.$detail,
$previous
);
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Server;
use AcmePhp\Core\Exception\AcmeCoreServerException;
use Psr\Http\Message\RequestInterface;
/**
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class TlsServerException extends AcmeCoreServerException
{
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
{
parent::__construct(
$request,
'[tls] The server experienced a TLS error during DV: '.$detail,
$previous
);
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Server;
use AcmePhp\Core\Exception\AcmeCoreServerException;
use Psr\Http\Message\RequestInterface;
/**
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class UnauthorizedServerException extends AcmeCoreServerException
{
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
{
parent::__construct(
$request,
'[unauthorized] The client lacks sufficient authorization: '.$detail,
$previous
);
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Server;
use AcmePhp\Core\Exception\AcmeCoreServerException;
use Psr\Http\Message\RequestInterface;
/**
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class UnknownHostServerException extends AcmeCoreServerException
{
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
{
parent::__construct(
$request,
'[unknownHost] The server could not resolve a domain name: '.$detail,
$previous
);
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Server;
use AcmePhp\Core\Exception\AcmeCoreServerException;
use Psr\Http\Message\RequestInterface;
/**
* @author Alex Plekhanov <alex@plekhanov.dev>
*/
class UnsupportedContactServerException extends AcmeCoreServerException
{
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
{
parent::__construct(
$request,
'[unsupportedContact] A contact URL for an account used an unsupported protocol scheme: '.$detail,
$previous
);
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Acme PHP project.
*
* (c) Titouan Galopin <galopintitouan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AcmePhp\Core\Exception\Server;
use AcmePhp\Core\Exception\AcmeCoreServerException;
use Psr\Http\Message\RequestInterface;
/**
* @author Alex Plekhanov <alex@plekhanov.dev>
*/
class UnsupportedIdentifierServerException extends AcmeCoreServerException
{
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
{
parent::__construct(
$request,
'[unsupportedIdentifier] An identifier is of an unsupported type: '.$detail,
$previous
);
}
}

Some files were not shown because too many files have changed in this diff Show More