[enh] first version of class-and-hooks-based update_domains.php, includes ssl certficate mechanism

This commit is contained in:
Benjamin Sonntag 2018-07-08 13:01:35 +02:00
parent a194cd80d0
commit ed7aaa3151
5 changed files with 415 additions and 19 deletions

179
bureau/class/m_apache.php Normal file
View File

@ -0,0 +1,179 @@
<?php
/*
----------------------------------------------------------------------
LICENSE
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License (GPL)
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
To read the license please visit http://www.gnu.org/copyleft/gpl.html
----------------------------------------------------------------------
*/
/**
* Manages APACHE 2.4+ vhosts templates in AlternC 3.5+
*
* @copyright AlternC-Team 2000-2018 https://alternc.com/
*/
class m_apache {
var $shouldreload;
// only values allowed for https in subdomaines table.
var $httpsmodes=array("http","https","both");
// Slave AlternC instances can know the last reload time thanks to this
var $reloadfile="/run/alternc/apache-reload";
// Where do we find apache template files ?
var $templatedir="/etc/alternc/templates/apache2";
// Where do we store all Apache vhosts ?
var $vhostroot="/var/lib/alternc/apache-vhost/";
// launched before any action by updatedomains
function hook_updatedomains_web_pre() {
$this->shouldreload=false;
}
// launched for each FQDN for which we want a new vhost template
function hook_updatedomains_web_add($subdomid) {
global $msg,$db;
$db->query("SELECT sd.*, dt.only_dns, dt.has_https_option, m.login FROM domaines_type dt, sub_domaines sd LEFT JOIN membres m ON m.uid=sd.compte WHERE dt.name=sd.type AND sd.web_action!='OK' AND id=?;",array($subdomid));
$db->next_record();
$subdom=$db->Record;
// security : only AlternC account's UIDs
if ($subdom["compte"]<1999) {
$msg->raise("ERROR","apache","Subdom ".$subdom["id"]." for domain ".$subdom["sub"].".".$subdom["domaine"]." has id ".$subdom["compte"].". Skipped");
return 1;
}
// search for the template file:
$template = $this->templatedir."/".strtolower($subdom["type"]);
if ($subdom["has_https_option"] && in_array($subdom["https"],$this->httpsmodes)) {
$template.="-".$subdom["https"];
}
$template.=".conf";
if (!is_file($template)) {
$msg->raise("ERROR","apache","Template $template not found for subdom ".$subdom["id"]." for domain ".$subdom["sub"].".".$subdom["domaine"].". Skipped");
return 1;
}
$subdom["fqdn"]=$subdom["sub"].(($subdom["sub"])?".":"").$subdom["domaine"];
// SSL information $subdom["certificate_id"] may be ZERO => it means "take id 0 which is snakeoil cert"
$cert = $ssl->get_certificate_path($subdom["certificate_id"]);
if ($cert["chain"]) {
$chainline="SSLCertificateChainFile ".$cert["chain"];
} else {
$chainline="";
}
// Replace needed vars in template file
$tpl=file_get_contents($template);
$tpl = strtr($tpl, array(
"%%LOGIN%%" => $subdom['login'],
"%%fqdn%%" => $subdom['fqdn'],
"%%document_root%%" => getuserpath($subdom['login']) . $subdom['valeur'],
"%%account_root%%" => getuserpath($subdom['login']),
"%%redirect%%" => $subdom['valeur'],
"%%UID%%" => $subdom['compte'],
"%%GID%%" => $subdom['compte'],
"%%mail_account%%" => $subdom['mail'],
"%%user%%" => "FIXME",
"%%CRT%%" => $cert["cert"],
"%%KEY%%" => $cert["key"],
"%%CHAINLINE%%" => $chainline,
));
// and write the template
$confdir = $this->vhostroot."/".substr($subdom["compte"],-1)."/".$subdom["compte"];
@mkdir($confdir,0755,true);
file_put_contents($confdir."/".$subdom["fqdn"].".conf");
$this->shouldreload=true;
return 0; // shell meaning => OK ;)
} // hook_updatedomains_web_add
// ------------------------------------------------------------
/**
* launched for each FQDN for which we want to delete a vhost template
*/
function hook_updatedomains_web_del($subdom) {
$confdir = $this->vhostroot."/".substr($subdom["compte"],-1)."/".$subdom["compte"];
@unlink($confdir."/".$subdom["fqdn"].".conf");
$this->shouldreload=true;
}
// ------------------------------------------------------------
/**
* launched at the very end of updatedomains
*/
function hook_updatedomains_web_post() {
global $msg;
if ($this->shouldreload) {
// concatenate all files into one
$this->concat();
// reload apache
$ret=0;
exec("apache2ctl graceful 2>&1",$out,$ret);
touch($this->reloadfile);
if ($ret!=0) {
$msg->raise("ERROR","apache","Error while reloading apache, error code is $ret\n".implode("\n",$out));
} else {
$msg->raise("INFO","apache","Apache reloaded");
}
}
}
// ------------------------------------------------------------
/**
* Concatenate all files under $this->vhostroot
* into one (mindepth=2 though),
* this function is faster than any shell stuff :D
*/
private function concat() {
global $msg;
$d=opendir($this->vhostroot);
$f=fopen($this->vhostroot."/vhosts_all.conf.new","wb");
if (!$f) {
$msg->raise("FATAL","apache","Can't write vhosts_all file");
return false;
}
while (($c=readdir($d))!==false) {
if (substr($c,0,1)!="." && is_dir($this->vhostroot."/".$c)) {
$this->subconcat($f,$this->vhostroot."/".$c);
}
}
closedir($d);
fclose($f);
}
private function subconcat($f,$root) {
// recursive cat :)
$d=opendir($root);
while (($c=readdir($d))!==false) {
if (substr($c,0,1)!=".") {
if (is_dir($root."/".$c)) {
$this->subconcat($f,$root."/".$c); // RECURSIVE CALL
}
if (is_file($root."/".$c)) {
fputs($f,file_get_contents($root."/".$c)."\n");
}
}
}
closedir($d);
}
} // m_apache

View File

@ -19,11 +19,16 @@
*/
/**
* bind9 file management class
* Manages BIND 9+ zone management templates in AlternC 3.5+
*
* @copyright AlternC-Team 2000-2017 https://alternc.com/
* @copyright AlternC-Team 2000-2018 https://alternc.com/
*/
class system_bind {
class m_bind {
var $shouldreload;
var $shouldreconfig;
var $ZONE_TEMPLATE ="/etc/alternc/templates/bind/templates/zone.template";
var $NAMED_TEMPLATE ="/etc/alternc/templates/bind/templates/named.template";
var $NAMED_CONF ="/var/lib/alternc/bind/automatic.conf";
@ -39,6 +44,47 @@ class system_bind {
var $cache_domain_summary = array();
var $zone_file_directory = '/var/lib/alternc/bind/zones/';
// launched before any action by updatedomains
function hook_updatedomains_dns_pre() {
$this->shouldreload=false;
$this->shouldreconfig=false;
}
// launched for each ZONE for which we want a zone update (or create)
function hook_updatedomains_dns_add($domain) {
}
// launched for each ZONE for which we want a zone DELETE
function hook_updatedomains_dns_del($domain) {
}
// launched at the very end of updatedomains
function hook_updatedomains_dns_post() {
global $msg;
if ($this->shouldreload) {
$ret=0;
exec($this->rndc." reload 2>&1",$out,$ret);
if ($ret!=0) {
$msg->raise("ERROR","bind","Error while reloading bind, error code is $ret\n".implode("\n",$out));
} else {
$msg->raise("INFO","bind","Bind reloaded");
}
}
if ($this->shouldreconfig) {
$ret=0;
exec($this->rndc." reload 2>&1",$out,$ret);
if ($ret!=0) {
$msg->raise("ERROR","bind","Error while reconfiguring bind, error code is $ret\n".implode("\n",$out));
} else {
$msg->raise("INFO","bind","Bind reconfigured");
}
}
}
/**
* Return the part of the conf we got from the database
@ -521,5 +567,6 @@ class system_bind {
}
} /* Class system_bind */
} // m_bind

View File

@ -1770,10 +1770,15 @@ class m_dom {
if ($this->islocked) {
$msg->raise("ERROR", "dom", _("--- Program error --- Lock already obtained!"));
}
while (file_exists($this->fic_lock_cron)) {
// wait for the file to disappear, or at most 15min:
while (file_exists($this->fic_lock_cron) && filemtime($this->fic_lock_cron)>(time()-900)) {
clearstatcache();
sleep(2);
}
@touch($this->fic_lock_cron);
$this->islocked = true;
// extra safe :
register_shutdown_function(array("m_dom","unlock"),1);
return true;
}
@ -1783,12 +1788,13 @@ class m_dom {
* return true
* @access private
*/
function unlock() {
function unlock($isshutdown=0) {
global $msg;
$msg->debug("dom", "unlock");
if (!$this->islocked) {
if (!$isshutdown && !$this->islocked) {
$msg->raise("ERROR", "dom", _("--- Program error --- No lock on the domains!"));
}
@unlink($this->fic_lock_cron);
$this->islocked = false;
return true;
}
@ -1891,6 +1897,95 @@ class m_dom {
}
/**
* complex process to manage domain and subdomain updates
* Launched every minute by a cron as root
* should launch hooks for each domain or subdomain,
* so that apache & bind could do their job
*/
function update_domains() {
if (posix_getuid()!=0) {
echo "FATAL: please lauch me as root\n";
exit();
}
$dom->lock();
// fix in case we forgot to delete SUBDOMAINS before deleting a DOMAIN
$db->query("UPDATE sub_domaines sd, domaines d SET sd.web_action = 'DELETE' WHERE sd.domaine = d.domaine AND sd.compte=d.compte AND d.dns_action = 'DELETE';");
// Search for things to do on DOMAINS:
$db->query("SELECT * FROM domaines WHERE dns_action!='OK';");
$alldoms=array();
while ($db->next_record()) {
$alldoms[$db->Record["id"]]=$db->Record;
}
// now launch hooks
if (count($alldoms)) {
$hooks->invoke("hook_updatedomains_dns_pre");
foreach($alldoms as $id=>$onedom) {
if ($onedom["gesdns"]==0 || $onedom["dns_action"]=="DELETE") {
$ret = $hooks->invoke("hook_updatedomains_dns_del",array(array($onedom)));
} else {
$ret = $hooks->invoke("hook_updatedomains_dns_add",array(array($onedom)));
}
if ($onedom["dns_action"]=="DELETE") {
$db->query("DELETE FROM domaines WHERE domaine=?;",array($onedom));
} else {
// we keep the highest result returned by hooks...
rsort($ret,SORT_NUMERIC); $returncode=$ret[0];
$db->query("UPDATE domaines SET dns_result=?, dns_action='OK' WHERE domaine=?;",array($returncode,$onedom));
}
}
$hooks->invoke("hook_updatedomains_dns_post");
}
// Search for things to do on SUB-DOMAINS:
$db->query("SELECT sd.*, dt.only_dns FROM domaines_type dt, sub_domaines sd WHERE dt.name=sd.type AND sd.web_action!='OK';");
$alldoms=array();
$ignore=array();
while ($db->next_record()) {
// only_dns=1 => weird, we should not have web_action SET to something else than OK ... anyway, skip it
if ($db->Record["only_dns"]) {
$ignore[]=$db->Record["id"];
} else {
$alldoms[$db->Record["id"]]=$db->Record;
}
}
foreach($ignore as $id) {
// @FIXME (unsure it's useful) maybe we could check that no file exist for this subdomain ?
$db->query("UPDATE sub_domaines SET web_action='OK' WHERE id=?;",array($id));
}
// now launch hooks
if (count($alldoms)) {
$hooks->invoke("hook_updatedomains_web_pre");
foreach($alldoms as $id=>$subdom) {
// is it a delete (DISABLED or DELETE)
if ($subdom["web_action"]=="DELETE" || strtoupper(substr($subdom["enable"],0,7))=="DISABLE") {
$ret = $hooks->invoke("hook_updatedomains_web_del",array($subdom["id"]));
} else {
$hooks->invoke("hook_updatedomains_web_before",array($subdom["id"])); // give a chance to get SSL cert before ;)
$ret = $hooks->invoke("hook_updatedomains_web_add",array($subdom["id"]));
$hooks->invoke("hook_updatedomains_web_after",array($subdom["id"]));
}
if ($subdom["web_action"]=="DELETE") {
$db->query("DELETE FROM sub_domaines WHERE id=?;",array($id));
} else {
// we keep the highest result returned by hooks...
rsort($ret,SORT_NUMERIC); $returncode=$ret[0];
$db->query("UPDATE sub_domaines SET web_result=?, web_action='OK' WHERE id=?;",array($returncode,$id));
}
}
$hooks->invoke("hook_updatedomains_web_post");
}
$dom->unlock();
}
/**
* Return an array with all the needed parameters to generate conf
* of a vhost.

View File

@ -372,7 +372,34 @@ INSTR(CONCAT(sd.sub,IF(sd.sub!='','.',''),sd.domaine),'.')+1))=?
return $db->Record;
}
// -----------------------------------------------------------------
/** Return paths to certificate, key, and chain for a certificate
* given it's ID.
* @param $id integer the certificate by id
* @return array cert, key, chain (not mandatory) with full path.
*/
function get_certificate_path($id) {
global $db, $msg, $cuid;
$msg->log("ssl", "get_certificate_path",$id);
$id = intval($id);
$db->query("SELECT id FROM certificates WHERE id=?;",array($id));
if (!$db->next_record()) {
$msg->raise("ERROR","ssl", _("Can't find this Certificate"));
// Return cert 0 info :)
$id=0;
}
$chain=self::KEY_REPOSITORY."/".floor($id/1000)."/".$id.".chain";
if (!file_exists($chain))
$chain=false;
return array(
"cert" => self::KEY_REPOSITORY."/".floor($id/1000)."/".$id.".cert",
"key" => self::KEY_REPOSITORY."/".floor($id/1000)."/".$id.".key",
"chain" => $chain
);
}
// -----------------------------------------------------------------
/** Return all the valid certificates that can be used for a specific FQDN
* return the list of certificates by order of preference
@ -492,6 +519,12 @@ INSTR(CONCAT(sd.sub,IF(sd.sub!='','.',''),sd.domaine),'.')+1))=?
$msg->raise("ERROR","ssl", _("Can't save the Key/Crt/Chain now. Please try later."));
return false;
}
$this->write_cert_file(array(
"id"=>$id,
"sslcrt"=>$crt,
"sslkey"=>$key,
"sslchain"=>$chain
));
return $id;
}
@ -612,6 +645,33 @@ SELECT ?,?,?, FROM_UNIXTIME(?), FROM_UNIXTIME(?), ?, ?, sslcsr FROM certificate
}
// -----------------------------------------------------------------
/** Launched by hosting_functions.sh launched by update_domaines.sh
* Action may be create/postinst/delete/enable/disable
* Change the template for this domain name to have the proper CERTIFICATE
* An algorithm determine the best possible certificate, which may be a BAD one
* (like a generic self-signed for localhost as a last chance)
*/
public function hook_updatedomains_web_before($subdomid) {
global $db, $msg, $dom;
$msg->log("ssl", "hook_updatedomains_web_before($subdomid)");
$db->query("SELECT sd.*, dt.only_dns, dt.has_https_option, m.login FROM domaines_type dt, sub_domaines sd LEFT JOIN membres m ON m.uid=sd.compte WHERE dt.name=sd.type AND sd.web_action!='OK' AND id=?;",array($subdomid));
$db->next_record();
$subdom=$db->Record;
$domtype=$dom->domains_type_get($subdom["type"]);
// the domain type must be a "dns_only=false" one:
if ($domtype["only_dns"]==true) {
return; // nothing to do : this domain type does not involve Vhosts
}
$subdom["fqdn"]=$subdom["sub"].(($subdom["sub"])?".":"").$subdom["domaine"];
list($cert) = $this->get_valid_certs($fqdn, $subdom["provider"]);
// Edit certif_hosts:
$db->query("UPDATE sub_domaines SET certificate_id=? WHERE id=?;",array($cert["id"], $subdom["id"]));
}
// ----------------------------------------------------------------
/** Search for the best certificate for a user and a fqdn
@ -627,7 +687,25 @@ SELECT ?,?,?, FROM_UNIXTIME(?), FROM_UNIXTIME(?), ?, ?, sslcsr FROM certificate
// get the first good certificate:
list($cert) = $this->get_valid_certs($fqdn, $subdom["provider"]);
$this->write_cert_file($cert);
// we have the files, let's fill the output array :
$output=array(
"id" => $cert["id"],
"crt" => $CRTDIR . "/" . $cert["id"].".pem",
"key" => $CRTDIR . "/" . $cert["id"].".key",
);
if (file_exists($CRTDIR . "/" . $cert["id"].".chain")) {
$output["chain"] = $CRTDIR . "/" . $cert["id"].".chain";
}
return $output;
}
// -----------------------------------------------------------------
/** Write certificate file into KEY_REPOSITORY
* @param $cert array an array with ID sslcrt sslkey sslchain
*/
function write_cert_file($cert) {
// we split the certificates by 1000
$CRTDIR = self::KEY_REPOSITORY . "/" . floor($cert["id"]/1000);
@mkdir($CRTDIR,0750,true);
@ -659,16 +737,6 @@ SELECT ?,?,?, FROM_UNIXTIME(?), FROM_UNIXTIME(?), ?, ?, sslcsr FROM certificate
chmod($CRTDIR . "/" . $cert["id"].".chain",0640);
}
}
// we have the files, let's fill the output array :
$output=array(
"id" => $cert["id"],
"crt" => $CRTDIR . "/" . $cert["id"].".pem",
"key" => $CRTDIR . "/" . $cert["id"].".key",
);
if (file_exists($CRTDIR . "/" . $cert["id"].".chain")) {
$output["chain"] = $CRTDIR . "/" . $cert["id"].".chain";
}
return $output;
}

View File

@ -1,5 +1,12 @@
#!/bin/bash
# Update domain next-gen by fufroma
# This is now done using PHP-only scripting
/usr/lib/alternc/update_domains.php
exit
# legacy code here
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
for CONFIG_FILE in \