Skip to content

Commit 746e79d

Browse files
author
Marius Burkard
committed
- backported acme.sh support from master branch, fixes #5461
1 parent 0c49824 commit 746e79d

File tree

3 files changed

+180
-65
lines changed

3 files changed

+180
-65
lines changed

server/lib/classes/cron.d/900-letsencrypt.inc.php

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ class cronjob_letsencrypt extends cronjob {
3535

3636
/* this function is optional if it contains no custom code */
3737
public function onPrepare() {
38-
global $app;
39-
4038
parent::onPrepare();
4139
}
4240

@@ -52,10 +50,19 @@ public function onRunJob() {
5250
global $app, $conf;
5351

5452
$server_config = $app->getconf->get_server_config($conf['server_id'], 'server');
55-
if(!isset($server_config['migration_mode']) || $server_config['migration_mode'] != 'y') {
56-
$letsencrypt = explode("\n", shell_exec('which letsencrypt certbot /root/.local/share/letsencrypt/bin/letsencrypt /opt/eff.org/certbot/venv/bin/certbot'));
57-
$letsencrypt = reset($letsencrypt);
58-
if(is_executable($letsencrypt)) {
53+
if(!isset($server_config['migration_mode']) || $server_config['migration_mode'] != 'y') {
54+
$acme = $app->letsencrypt->get_acme_script();
55+
if($acme) {
56+
// skip letsencrypt
57+
parent::onRunJob();
58+
return;
59+
}
60+
61+
$letsencrypt = $app->letsencrypt->get_certbot_script();
62+
if($letsencrypt) {
63+
$ret = null;
64+
$val = 0;
65+
$matches = array();
5966
$version = exec($letsencrypt . ' --version 2>&1', $ret, $val);
6067
if(preg_match('/^(\S+|\w+)\s+(\d+(\.\d+)+)$/', $version, $matches)) {
6168
$type = strtolower($matches[1]);
@@ -86,11 +93,7 @@ public function onRunJob() {
8693

8794
/* this function is optional if it contains no custom code */
8895
public function onAfterRun() {
89-
global $app;
90-
9196
parent::onAfterRun();
9297
}
9398

94-
}
95-
96-
?>
99+
}

server/lib/classes/letsencrypt.inc.php

Lines changed: 163 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,136 @@ class letsencrypt {
3737
*/
3838
private $base_path = '/etc/letsencrypt';
3939
private $renew_config_path = '/etc/letsencrypt/renewal';
40-
40+
private $certbot_use_certcommand = false;
4141

4242
public function __construct(){
4343

4444
}
45+
46+
public function get_acme_script() {
47+
$acme = explode("\n", shell_exec('which /usr/local/ispconfig/server/scripts/acme.sh /root/.acme.sh/acme.sh'));
48+
$acme = reset($acme);
49+
if(is_executable($acme)) {
50+
return $acme;
51+
} else {
52+
return false;
53+
}
54+
}
55+
56+
public function get_acme_command($domains, $key_file, $bundle_file, $cert_file) {
57+
58+
$letsencrypt = $this->get_acme_script();
59+
60+
$cmd = '';
61+
// generate cli format
62+
foreach($domains as $domain) {
63+
$cmd .= (string) " -d " . $domain;
64+
}
65+
66+
if($cmd == '') {
67+
return false;
68+
}
69+
70+
$cmd = 'R=0 ; C=0 ; ' . $letsencrypt . ' --issue ' . $cmd . ' -w /usr/local/ispconfig/interface/acme ; R=$? ; if [[ $R -eq 0 || $R -eq 2 ]] ; then ' . $letsencrypt . ' --install-cert ' . $cmd . ' --key-file ' . escapeshellarg($key_file) . ' --fullchain-file ' . escapeshellarg($bundle_file) . ' --cert-file ' . escapeshellarg($cert_file) . ' --reloadcmd ' . escapeshellarg($this->get_reload_command()) . '; C=$? ; fi ; if [[ $C -eq 0 ]] ; then exit $R ; else exit $C ; fi';
71+
72+
return $cmd;
73+
}
74+
75+
public function get_certbot_script() {
76+
$letsencrypt = explode("\n", shell_exec('which letsencrypt certbot /root/.local/share/letsencrypt/bin/letsencrypt /opt/eff.org/certbot/venv/bin/certbot'));
77+
$letsencrypt = reset($letsencrypt);
78+
if(is_executable($letsencrypt)) {
79+
return $letsencrypt;
80+
} else {
81+
return false;
82+
}
83+
}
4584

85+
private function install_acme() {
86+
$install_cmd = 'wget -O - https://get.acme.sh | sh';
87+
$ret = null;
88+
$val = 0;
89+
exec($install_cmd . ' 2>&1', $ret, $val);
90+
91+
return ($val == 0 ? true : false);
92+
}
93+
94+
private function get_reload_command() {
95+
global $app, $conf;
96+
97+
$web_config = $app->getconf->get_server_config($conf['server_id'], 'web');
98+
99+
$daemon = '';
100+
switch ($web_config['server_type']) {
101+
case 'nginx':
102+
$daemon = $web_config['server_type'];
103+
break;
104+
default:
105+
if(is_file($conf['init_scripts'] . '/' . 'httpd24-httpd') || is_dir('/opt/rh/httpd24/root/etc/httpd')) {
106+
$daemon = 'httpd24-httpd';
107+
} elseif(is_file($conf['init_scripts'] . '/' . 'httpd') || is_dir('/etc/httpd')) {
108+
$daemon = 'httpd';
109+
} else {
110+
$daemon = 'apache2';
111+
}
112+
}
113+
114+
$cmd = $app->system->getinitcommand($daemon, 'force-reload');
115+
return $cmd;
116+
}
117+
118+
public function get_certbot_command($domains) {
119+
global $app;
120+
121+
$letsencrypt = $this->get_certbot_script();
122+
123+
$cmd = '';
124+
// generate cli format
125+
foreach($domains as $domain) {
126+
$cmd .= (string) " --domains " . $domain;
127+
}
128+
129+
if($cmd == '') {
130+
return false;
131+
}
132+
133+
$matches = array();
134+
$ret = null;
135+
$val = 0;
136+
137+
$letsencrypt_version = exec($letsencrypt . ' --version 2>&1', $ret, $val);
138+
if(preg_match('/^(\S+|\w+)\s+(\d+(\.\d+)+)$/', $letsencrypt_version, $matches)) {
139+
$letsencrypt_version = $matches[2];
140+
}
141+
if (version_compare($letsencrypt_version, '0.22', '>=')) {
142+
$acme_version = 'https://acme-v02.api.letsencrypt.org/directory';
143+
} else {
144+
$acme_version = 'https://acme-v01.api.letsencrypt.org/directory';
145+
}
146+
if (version_compare($letsencrypt_version, '0.30', '>=')) {
147+
$app->log("LE version is " . $letsencrypt_version . ", so using certificates command", LOGLEVEL_DEBUG);
148+
$this->certbot_use_certcommand = true;
149+
$webroot_map = array();
150+
for($i = 0; $i < count($domains); $i++) {
151+
$webroot_map[$domains[$i]] = '/usr/local/ispconfig/interface/acme';
152+
}
153+
$webroot_args = "--webroot-map " . escapeshellarg(str_replace(array("\r", "\n"), '', json_encode($webroot_map)));
154+
} else {
155+
$webroot_args = "$cmd --webroot-path /usr/local/ispconfig/interface/acme";
156+
}
157+
158+
$cmd = $letsencrypt . " certonly -n --text --agree-tos --expand --authenticator webroot --server $acme_version --rsa-key-size 4096 --email postmaster@$domain $cmd --webroot-path /usr/local/ispconfig/interface/acme";
159+
160+
return $cmd;
161+
}
162+
46163
public function get_letsencrypt_certificate_paths($domains = array()) {
47164
global $app;
48165

166+
if($this->get_acme_script()) {
167+
return false;
168+
}
169+
49170
if(empty($domains)) return false;
50171
if(!is_dir($this->renew_config_path)) return false;
51172

@@ -133,9 +254,13 @@ public function get_letsencrypt_certificate_paths($domains = array()) {
133254
}
134255

135256
private function get_ssl_domain($data) {
136-
$domain = $data['new']['ssl_domain'];
137-
if(!$domain) $domain = $data['new']['domain'];
257+
global $app;
138258

259+
$domain = $data['new']['ssl_domain'];
260+
if(!$domain) {
261+
$domain = $data['new']['domain'];
262+
}
263+
139264
if($data['new']['ssl'] == 'y' && $data['new']['ssl_letsencrypt'] == 'y') {
140265
$domain = $data['new']['domain'];
141266
if(substr($domain, 0, 2) === '*.') {
@@ -149,8 +274,6 @@ private function get_ssl_domain($data) {
149274
}
150275

151276
public function get_website_certificate_paths($data) {
152-
global $app;
153-
154277
$ssl_dir = $data['new']['document_root'].'/ssl';
155278
$domain = $this->get_ssl_domain($data);
156279

@@ -183,11 +306,17 @@ public function request_certificates($data, $server_type = 'apache') {
183306
$web_config = $app->getconf->get_server_config($conf['server_id'], 'web');
184307
$server_config = $app->getconf->get_server_config($conf['server_id'], 'server');
185308

309+
$use_acme = false;
310+
if($this->get_acme_script()) {
311+
$use_acme = true;
312+
} elseif(!$this->get_certbot_script()) {
313+
// acme and le missing
314+
$this->install_acme();
315+
}
316+
186317
$tmp = $app->letsencrypt->get_website_certificate_paths($data);
187318
$domain = $tmp['domain'];
188319
$key_file = $tmp['key'];
189-
$key_file2 = $tmp['key2'];
190-
$csr_file = $tmp['csr'];
191320
$crt_file = $tmp['crt'];
192321
$bundle_file = $tmp['bundle'];
193322

@@ -256,66 +385,50 @@ public function request_certificates($data, $server_type = 'apache') {
256385
$app->log("There were " . $le_domain_count . " domains in the domain list. LE only supports 100, so we strip the rest.", LOGLEVEL_WARN);
257386
}
258387

259-
// generate cli format
260-
foreach($temp_domains as $temp_domain) {
261-
$cli_domain_arg .= (string) " --domains " . $temp_domain;
262-
}
263-
264388
// unset useless data
265389
unset($subdomains);
266390
unset($aliasdomains);
267391

268-
$letsencrypt_use_certcommand = false;
392+
$this->certbot_use_certcommand = false;
269393
$letsencrypt_cmd = '';
270-
$letsencrypt = false;
271-
$success = false;
272-
273-
$letsencrypt = explode("\n", shell_exec('which letsencrypt certbot /root/.local/share/letsencrypt/bin/letsencrypt /opt/eff.org/certbot/venv/bin/certbot'));
274-
$letsencrypt = reset($letsencrypt);
275-
if(!is_executable($letsencrypt)) {
276-
$letsencrypt = false;
394+
$allow_return_codes = null;
395+
if($use_acme) {
396+
$letsencrypt_cmd = $this->get_acme_command($temp_domains, $key_file, $bundle_file, $crt_file);
397+
$allow_return_codes = array(2);
398+
} else {
399+
$letsencrypt_cmd = $this->get_certbot_command($temp_domains);
277400
}
278-
if(!empty($cli_domain_arg)) {
401+
402+
$success = false;
403+
if($letsencrypt_cmd) {
279404
if(!isset($server_config['migration_mode']) || $server_config['migration_mode'] != 'y') {
280405
$app->log("Create Let's Encrypt SSL Cert for: $domain", LOGLEVEL_DEBUG);
281406
$app->log("Let's Encrypt SSL Cert domains: $cli_domain_arg", LOGLEVEL_DEBUG);
282-
283-
if($letsencrypt) {
284-
$letsencrypt_version = exec($letsencrypt . ' --version 2>&1', $ret, $val);
285-
if(preg_match('/^(\S+|\w+)\s+(\d+(\.\d+)+)$/', $letsencrypt_version, $matches)) {
286-
$letsencrypt_version = $matches[2];
287-
}
288-
if (version_compare($letsencrypt_version, '0.22', '>=')) {
289-
$acme_version = 'https://acme-v02.api.letsencrypt.org/directory';
290-
} else {
291-
$acme_version = 'https://acme-v01.api.letsencrypt.org/directory';
292-
}
293-
if (version_compare($letsencrypt_version, '0.30', '>=')) {
294-
$app->log("LE version is " . $letsencrypt_version . ", so using certificates command", LOGLEVEL_DEBUG);
295-
$letsencrypt_use_certcommand = true;
296-
$webroot_map = array();
297-
for($i = 0; $i < count($temp_domains); $i++) {
298-
$webroot_map[$temp_domains[$i]] = '/usr/local/ispconfig/interface/acme';
299-
}
300-
$webroot_args = "--webroot-map " . escapeshellarg(str_replace(array("\r", "\n"), '', json_encode($webroot_map)));
301-
} else {
302-
$webroot_args = "$cli_domain_arg --webroot-path /usr/local/ispconfig/interface/acme";
303-
}
304-
305-
$letsencrypt_cmd = $letsencrypt . " certonly -n --text --agree-tos --expand --authenticator webroot --server $acme_version --rsa-key-size 4096 --email postmaster@$domain $webroot_args";
306-
$success = $app->system->_exec($letsencrypt_cmd);
307-
}
407+
408+
$success = $app->system->_exec($letsencrypt_cmd, $allow_return_codes);
308409
} else {
309410
$app->log("Migration mode active, skipping Let's Encrypt SSL Cert creation for: $domain", LOGLEVEL_DEBUG);
310411
$success = true;
311412
}
312413
}
414+
415+
if($use_acme === true) {
416+
if(!$success) {
417+
$app->log('Let\'s Encrypt SSL Cert for: ' . $domain . ' could not be issued.', LOGLEVEL_WARN);
418+
$app->log($letsencrypt_cmd, LOGLEVEL_WARN);
419+
return false;
420+
} else {
421+
return true;
422+
}
423+
}
424+
313425
$le_files = array();
314-
if($letsencrypt_use_certcommand === true && $letsencrypt) {
315-
$letsencrypt_cmd = $letsencrypt . " certificates " . $cli_domain_arg;
426+
if($this->certbot_use_certcommand === true && $letsencrypt_cmd) {
427+
$letsencrypt_cmd = $letsencrypt_cmd . " certificates " . $cli_domain_arg;
316428
$output = explode("\n", shell_exec($letsencrypt_cmd . " 2>/dev/null | grep -v '^\$'"));
317429
$le_path = '';
318430
$skip_to_next = true;
431+
$matches = null;
319432
foreach($output as $outline) {
320433
$outline = trim($outline);
321434
$app->log("LE CERT OUTPUT: " . $outline, LOGLEVEL_DEBUG);
@@ -415,6 +528,4 @@ public function request_certificates($data, $server_type = 'apache') {
415528
return false;
416529
}
417530
}
418-
}
419-
420-
?>
531+
}

server/lib/classes/system.inc.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1742,13 +1742,14 @@ function mkdirpath($path, $mode = 0755, $user = '', $group = '', $run_as_user =
17421742

17431743
}
17441744

1745-
function _exec($command) {
1745+
function _exec($command, $allow_return_codes = null) {
17461746
global $app;
17471747
$out = array();
17481748
$ret = 0;
17491749
$app->log('exec: '.$command, LOGLEVEL_DEBUG);
17501750
exec($command, $out, $ret);
1751-
if($ret != 0) return false;
1751+
if(is_array($allow_return_codes) && in_array($ret, $allow_return_codes)) return true;
1752+
elseif($ret != 0) return false;
17521753
else return true;
17531754
}
17541755

0 commit comments

Comments
 (0)