Skip to content

Commit d75d922

Browse files
author
Marius Burkard
committed
- added missing file from master
1 parent aac9cba commit d75d922

File tree

1 file changed

+351
-0
lines changed

1 file changed

+351
-0
lines changed
Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
<?php
2+
3+
/*
4+
Copyright (c) 2017, Marius Burkard, projektfarm Gmbh
5+
All rights reserved.
6+
7+
Redistribution and use in source and binary forms, with or without modification,
8+
are permitted provided that the following conditions are met:
9+
10+
* Redistributions of source code must retain the above copyright notice,
11+
this list of conditions and the following disclaimer.
12+
* Redistributions in binary form must reproduce the above copyright notice,
13+
this list of conditions and the following disclaimer in the documentation
14+
and/or other materials provided with the distribution.
15+
* Neither the name of ISPConfig nor the names of its contributors
16+
may be used to endorse or promote products derived from this software without
17+
specific prior written permission.
18+
19+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22+
IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
23+
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24+
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
26+
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
28+
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
31+
class letsencrypt {
32+
33+
/**
34+
* Construct for this class
35+
*
36+
* @return system
37+
*/
38+
private $base_path = '/etc/letsencrypt';
39+
private $renew_config_path = '/etc/letsencrypt/renewal';
40+
41+
42+
public function __construct(){
43+
44+
}
45+
46+
public function get_letsencrypt_certificate_paths($domains = array()) {
47+
global $app;
48+
49+
if(empty($domains)) return false;
50+
if(!is_dir($this->renew_config_path)) return false;
51+
52+
$dir = opendir($this->renew_config_path);
53+
if(!$dir) return false;
54+
55+
$path_scores = array();
56+
57+
$main_domain = reset($domains);
58+
sort($domains);
59+
$min_diff = false;
60+
61+
while($file = readdir($dir)) {
62+
if($file === '.' || $file === '..' || substr($file, -5) !== '.conf') continue;
63+
$file_path = $this->renew_config_path . '/' . $file;
64+
if(!is_file($file_path) || !is_readable($file_path)) continue;
65+
66+
$fp = fopen($file_path, 'r');
67+
if(!$fp) continue;
68+
69+
$path_scores[$file_path] = array(
70+
'domains' => array(),
71+
'diff' => 0,
72+
'has_main_domain' => false,
73+
'cert_paths' => array(
74+
'cert' => '',
75+
'privkey' => '',
76+
'chain' => '',
77+
'fullchain' => ''
78+
)
79+
);
80+
$in_list = false;
81+
while(!feof($fp) && $line = fgets($fp)) {
82+
$line = trim($line);
83+
if($line === '') continue;
84+
elseif(!$in_list) {
85+
if($line == '[[webroot_map]]') $in_list = true;
86+
87+
$tmp = explode('=', $line, 2);
88+
if(count($tmp) != 2) continue;
89+
$key = trim($tmp[0]);
90+
if($key == 'cert' || $key == 'privkey' || $key == 'chain' || $key == 'fullchain') {
91+
$path_scores[$file_path]['cert_paths'][$key] = trim($tmp[1]);
92+
}
93+
94+
continue;
95+
}
96+
97+
$tmp = explode('=', $line, 2);
98+
if(count($tmp) != 2) continue;
99+
100+
$domain = trim($tmp[0]);
101+
if($domain == $main_domain) $path_scores[$file_path]['has_main_domain'] = true;
102+
$path_scores[$file_path]['domains'][] = $domain;
103+
}
104+
fclose($fp);
105+
106+
sort($path_scores[$file_path]['domains']);
107+
if(count(array_intersect($domains, $path_scores[$file_path]['domains'])) < 1) {
108+
$path_scores[$file_path]['diff'] = false;
109+
} else {
110+
// give higher diff value to missing domains than to those that are too much in there
111+
$path_scores[$file_path]['diff'] = (count(array_diff($domains, $path_scores[$file_path]['domains'])) * 1.5) + count(array_diff($path_scores[$file_path]['domains'], $domains));
112+
}
113+
114+
if($min_diff === false || $path_scores[$file_path]['diff'] < $min_diff) $min_diff = $path_scores[$file_path]['diff'];
115+
}
116+
closedir($dir);
117+
118+
if($min_diff === false) return false;
119+
120+
$cert_paths = false;
121+
$used_path = false;
122+
foreach($path_scores as $path => $data) {
123+
if($data['diff'] === $min_diff) {
124+
$used_path = $path;
125+
$cert_paths = $data['cert_paths'];
126+
if($data['has_main_domain'] == true) break;
127+
}
128+
}
129+
130+
$app->log("Let's Encrypt Cert config path is: " . ($used_path ? $used_path : "not found") . ".", LOGLEVEL_DEBUG);
131+
132+
return $cert_paths;
133+
}
134+
135+
private function get_ssl_domain($data) {
136+
$domain = $data['new']['ssl_domain'];
137+
if(!$domain) $domain = $data['new']['domain'];
138+
139+
if($data['new']['ssl'] == 'y' && $data['new']['ssl_letsencrypt'] == 'y') {
140+
$domain = $data['new']['domain'];
141+
if(substr($domain, 0, 2) === '*.') {
142+
// wildcard domain not yet supported by letsencrypt!
143+
$app->log('Wildcard domains not yet supported by letsencrypt, so changing ' . $domain . ' to ' . substr($domain, 2), LOGLEVEL_WARN);
144+
$domain = substr($domain, 2);
145+
}
146+
}
147+
148+
return $domain;
149+
}
150+
151+
public function get_website_certificate_paths($data) {
152+
global $app;
153+
154+
$ssl_dir = $data['new']['document_root'].'/ssl';
155+
$domain = $this->get_ssl_domain($data);
156+
157+
$cert_paths = array(
158+
'domain' => $domain,
159+
'key' => $ssl_dir.'/'.$domain.'.key',
160+
'key2' => $ssl_dir.'/'.$domain.'.key.org',
161+
'csr' => $ssl_dir.'/'.$domain.'.csr',
162+
'crt' => $ssl_dir.'/'.$domain.'.crt',
163+
'bundle' => $ssl_dir.'/'.$domain.'.bundle'
164+
);
165+
166+
if($data['new']['ssl'] == 'y' && $data['new']['ssl_letsencrypt'] == 'y') {
167+
$cert_paths = array(
168+
'domain' => $domain,
169+
'key' => $ssl_dir.'/'.$domain.'-le.key',
170+
'key2' => $ssl_dir.'/'.$domain.'-le.key.org',
171+
'crt' => $ssl_dir.'/'.$domain.'-le.crt',
172+
'bundle' => $ssl_dir.'/'.$domain.'-le.bundle'
173+
);
174+
}
175+
176+
return $cert_paths;
177+
}
178+
179+
public function request_certificates($data, $server_type = 'apache') {
180+
global $app, $conf;
181+
182+
$app->uses('getconf');
183+
$web_config = $app->getconf->get_server_config($conf['server_id'], 'web');
184+
185+
$tmp = $app->letsencrypt->get_website_certificate_paths($data);
186+
$domain = $tmp['domain'];
187+
$key_file = $tmp['key'];
188+
$key_file2 = $tmp['key2'];
189+
$csr_file = $tmp['csr'];
190+
$crt_file = $tmp['crt'];
191+
$bundle_file = $tmp['bundle'];
192+
193+
// default values
194+
$temp_domains = array($domain);
195+
$cli_domain_arg = '';
196+
$subdomains = null;
197+
$aliasdomains = null;
198+
199+
//* be sure to have good domain
200+
if(substr($domain,0,4) != 'www.' && ($data['new']['subdomain'] == "www" || $data['new']['subdomain'] == "*")) {
201+
$temp_domains[] = "www." . $domain;
202+
}
203+
204+
//* then, add subdomain if we have
205+
$subdomains = $app->db->queryAllRecords('SELECT domain FROM web_domain WHERE parent_domain_id = '.intval($data['new']['domain_id'])." AND active = 'y' AND type = 'subdomain'");
206+
if(is_array($subdomains)) {
207+
foreach($subdomains as $subdomain) {
208+
$temp_domains[] = $subdomain['domain'];
209+
}
210+
}
211+
212+
//* then, add alias domain if we have
213+
$aliasdomains = $app->db->queryAllRecords('SELECT domain,subdomain FROM web_domain WHERE parent_domain_id = '.intval($data['new']['domain_id'])." AND active = 'y' AND type = 'alias'");
214+
if(is_array($aliasdomains)) {
215+
foreach($aliasdomains as $aliasdomain) {
216+
$temp_domains[] = $aliasdomain['domain'];
217+
if(isset($aliasdomain['subdomain']) && substr($aliasdomain['domain'],0,4) != 'www.' && ($aliasdomain['subdomain'] == "www" OR $aliasdomain['subdomain'] == "*")) {
218+
$temp_domains[] = "www." . $aliasdomain['domain'];
219+
}
220+
}
221+
}
222+
223+
// prevent duplicate
224+
$temp_domains = array_unique($temp_domains);
225+
226+
// check if domains are reachable to avoid letsencrypt verification errors
227+
$le_rnd_file = uniqid('le-') . '.txt';
228+
$le_rnd_hash = md5(uniqid('le-', true));
229+
if(!is_dir('/usr/local/interface/acme/.well-known/acme-challenge/')) {
230+
$app->system->mkdir('/usr/local/ispconfig/interface/acme/.well-known/acme-challenge/', false, 0755, true);
231+
}
232+
file_put_contents('/usr/local/ispconfig/interface/acme/.well-known/acme-challenge/' . $le_rnd_file, $le_rnd_hash);
233+
234+
$le_domains = array();
235+
foreach($temp_domains as $temp_domain) {
236+
if(isset($web_config['skip_le_check']) && $web_config['skip_le_check'] == 'y') {
237+
$le_domains[] = $temp_domain;
238+
} else {
239+
$le_hash_check = trim(@file_get_contents('http://' . $temp_domain . '/.well-known/acme-challenge/' . $le_rnd_file));
240+
if($le_hash_check == $le_rnd_hash) {
241+
$le_domains[] = $temp_domain;
242+
$app->log("Verified domain " . $temp_domain . " should be reachable for letsencrypt.", LOGLEVEL_DEBUG);
243+
} else {
244+
$app->log("Could not verify domain " . $temp_domain . ", so excluding it from letsencrypt request.", LOGLEVEL_WARN);
245+
}
246+
}
247+
}
248+
$temp_domains = $le_domains;
249+
unset($le_domains);
250+
@unlink('/usr/local/ispconfig/interface/acme/.well-known/acme-challenge/' . $le_rnd_file);
251+
252+
// generate cli format
253+
foreach($temp_domains as $temp_domain) {
254+
$cli_domain_arg .= (string) " --domains " . $temp_domain;
255+
}
256+
257+
$le_files = $this->get_letsencrypt_certificate_paths($temp_domains);
258+
259+
// unset useless data
260+
unset($subdomains);
261+
unset($aliasdomains);
262+
unset($temp_domains);
263+
264+
if($server_type != 'apache' || version_compare($app->system->getapacheversion(true), '2.4.8', '>=')) {
265+
$crt_tmp_file = $le_files['fullchain'];
266+
} else {
267+
$crt_tmp_file = $le_files['cert'];
268+
}
269+
270+
$key_tmp_file = $le_files['privkey'];
271+
$bundle_tmp_file = $le_files['chain'];
272+
273+
$letsencrypt_cmd = '';
274+
$success = false;
275+
if(!empty($cli_domain_arg)) {
276+
$app->log("Create Let's Encrypt SSL Cert for: $domain", LOGLEVEL_DEBUG);
277+
$app->log("Let's Encrypt SSL Cert domains: $cli_domain_arg", LOGLEVEL_DEBUG);
278+
279+
$letsencrypt = explode("\n", shell_exec('which letsencrypt certbot /root/.local/share/letsencrypt/bin/letsencrypt'));
280+
$letsencrypt = reset($letsencrypt);
281+
if(is_executable($letsencrypt)) {
282+
$letsencrypt_cmd = $letsencrypt . " certonly -n --text --agree-tos --expand --authenticator webroot --server https://acme-v01.api.letsencrypt.org/directory --rsa-key-size 4096 --email postmaster@$domain $cli_domain_arg --webroot-path /usr/local/ispconfig/interface/acme";
283+
$success = $app->system->_exec($letsencrypt_cmd);
284+
}
285+
}
286+
287+
if(!$success) {
288+
// error issuing cert
289+
$app->log('Let\'s Encrypt SSL Cert for: ' . $domain . ' could not be issued.', LOGLEVEL_WARN);
290+
$app->log($letsencrypt_cmd, LOGLEVEL_WARN);
291+
292+
// if cert already exists, dont remove it. Ex. expired/misstyped/noDnsYet alias domain, api down...
293+
if(!file_exists($crt_tmp_file)) {
294+
return false;
295+
}
296+
}
297+
298+
//* check is been correctly created
299+
if(file_exists($crt_tmp_file)) {
300+
$app->log("Let's Encrypt Cert file: $crt_tmp_file exists.", LOGLEVEL_DEBUG);
301+
$date = date("YmdHis");
302+
303+
//* TODO: check if is a symlink, if target same keep it, either remove it
304+
if(is_file($key_file)) {
305+
$app->system->copy($key_file, $key_file.'.old.'.$date);
306+
$app->system->chmod($key_file.'.old.'.$date, 0400);
307+
$app->system->unlink($key_file);
308+
}
309+
310+
if ($web_config["website_symlinks_rel"] == 'y') {
311+
$app->system->create_relative_link(escapeshellcmd($key_tmp_file), escapeshellcmd($key_file));
312+
} else {
313+
if(@is_link($key_file)) $app->system->unlink($key_file);
314+
if(@file_exists($key_tmp_file)) exec("ln -s ".escapeshellcmd($key_tmp_file)." ".escapeshellcmd($key_file));
315+
}
316+
317+
if(is_file($crt_file)) {
318+
$app->system->copy($crt_file, $crt_file.'.old.'.$date);
319+
$app->system->chmod($crt_file.'.old.'.$date, 0400);
320+
$app->system->unlink($crt_file);
321+
}
322+
323+
if($web_config["website_symlinks_rel"] == 'y') {
324+
$app->system->create_relative_link(escapeshellcmd($crt_tmp_file), escapeshellcmd($crt_file));
325+
} else {
326+
if(@is_link($crt_file)) $app->system->unlink($crt_file);
327+
if(@file_exists($crt_tmp_file))exec("ln -s ".escapeshellcmd($crt_tmp_file)." ".escapeshellcmd($crt_file));
328+
}
329+
330+
if(is_file($bundle_file)) {
331+
$app->system->copy($bundle_file, $bundle_file.'.old.'.$date);
332+
$app->system->chmod($bundle_file.'.old.'.$date, 0400);
333+
$app->system->unlink($bundle_file);
334+
}
335+
336+
if($web_config["website_symlinks_rel"] == 'y') {
337+
$app->system->create_relative_link(escapeshellcmd($bundle_tmp_file), escapeshellcmd($bundle_file));
338+
} else {
339+
if(@is_link($bundle_file)) $app->system->unlink($bundle_file);
340+
if(@file_exists($bundle_tmp_file)) exec("ln -s ".escapeshellcmd($bundle_tmp_file)." ".escapeshellcmd($bundle_file));
341+
}
342+
343+
return true;
344+
} else {
345+
$app->log("Let's Encrypt Cert file: $crt_tmp_file does not exist.", LOGLEVEL_DEBUG);
346+
return false;
347+
}
348+
}
349+
}
350+
351+
?>

0 commit comments

Comments
 (0)