From ed7aaa3151ae946f61ebfa67f57c1d7d9dee654f Mon Sep 17 00:00:00 2001 From: Benjamin Sonntag Date: Sun, 8 Jul 2018 13:01:35 +0200 Subject: [PATCH] [enh] first version of class-and-hooks-based update_domains.php, includes ssl certficate mechanism --- bureau/class/m_apache.php | 179 ++++++++++++++++++ .../{class_system_bind.php => m_bind.php} | 55 +++++- bureau/class/m_dom.php | 101 +++++++++- bureau/class/m_ssl.php | 90 +++++++-- src/update_domains.sh | 9 +- 5 files changed, 415 insertions(+), 19 deletions(-) create mode 100644 bureau/class/m_apache.php rename bureau/class/{class_system_bind.php => m_bind.php} (92%) diff --git a/bureau/class/m_apache.php b/bureau/class/m_apache.php new file mode 100644 index 00000000..e497e8e1 --- /dev/null +++ b/bureau/class/m_apache.php @@ -0,0 +1,179 @@ +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 + diff --git a/bureau/class/class_system_bind.php b/bureau/class/m_bind.php similarity index 92% rename from bureau/class/class_system_bind.php rename to bureau/class/m_bind.php index 34d045b4..c5080629 100644 --- a/bureau/class/class_system_bind.php +++ b/bureau/class/m_bind.php @@ -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 diff --git a/bureau/class/m_dom.php b/bureau/class/m_dom.php index 64d50a7f..bafab4de 100644 --- a/bureau/class/m_dom.php +++ b/bureau/class/m_dom.php @@ -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. diff --git a/bureau/class/m_ssl.php b/bureau/class/m_ssl.php index 109241c4..69b0a6f0 100644 --- a/bureau/class/m_ssl.php +++ b/bureau/class/m_ssl.php @@ -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; } diff --git a/src/update_domains.sh b/src/update_domains.sh index feb2f923..3869ac72 100755 --- a/src/update_domains.sh +++ b/src/update_domains.sh @@ -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 \