diff --git a/bureau/class/functions.php b/bureau/class/functions.php index b9c94817..cd9e782c 100755 --- a/bureau/class/functions.php +++ b/bureau/class/functions.php @@ -1245,3 +1245,54 @@ function _dovecot_hash($password) { $hash = _sha512cr($password); return '{SHA512-CRYPT}' . $hash; } + + +// ------------------------------------------------------------ +/** + * Add the line of text $line into file $file. + * do not duplicate (check) + * @param $file string the full path to the file where we should add the line + * @param $line string the line to add (without the termination \n, WILL BE ADDED) + * @return boolean TRUE if the line has been added, or FALSE if the line ALREADY EXISTED + */ +function add_line_to_file($file,$line) { + $f=fopen($file,"rb"); + $found=false; + while($s=fgets($f,1024)) { + if (trim($s)==$line) { + $found=true; + return false; + } + } + fclose($f); + $f=fopen($file,"ab"); + fputs($f,trim($line)."\n"); + fclose($f); + return true; +} + + +// ------------------------------------------------------------ +/** + * Remove the line of text $line from file $file. + * @param $file string the full path to the file where we should remove the line + * @param $line string the line to add (without the termination \n, WILL BE REMOVED) + * @return boolean TRUE if the line has been found and removed, or FALSE if the line DIDN'T EXIST + */ +function del_line_from_file($file,$line) { + $f=fopen($file,"rb"); + $g=fopen($file.".new","wb"); + $found=false; + while($s=fgets($f,1024)) { + if (trim($s)!=$line) { + fputs($g,$s); + } else { + $found=true; + } + } + fclose($f); + fclose($g); + rename($file.".new",$file); // overwrite atomically + return $found; +} + diff --git a/bureau/class/m_bind.php b/bureau/class/m_bind.php index c5080629..e85b42ad 100644 --- a/bureau/class/m_bind.php +++ b/bureau/class/m_bind.php @@ -27,41 +27,132 @@ 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"; var $RNDC ="/usr/sbin/rndc"; - var $dkim_trusted_host_file = "/etc/opendkim/TrustedHosts"; - var $dkim_keytable_file = "/etc/opendkim/KeyTable"; - var $dkim_signingtable_file = "/etc/opendkim/SigningTable"; + var $zone_file_directory = '/var/lib/alternc/bind/zones'; - var $cache_conf_db = array(); - var $cache_get_persistent = array(); - var $cache_zone_file = array(); - var $cache_domain_summary = array(); - var $zone_file_directory = '/var/lib/alternc/bind/zones/'; - - - // launched before any action by updatedomains + // ------------------------------------------------------------ + /** Hook launched before any action by updatedomains + * initialize the reload/reconfig flags used by POST + * @NOTE launched as ROOT + */ 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) { - + + // ------------------------------------------------------------ + /** + * Hook launched for each ZONE for which we want a zone update (or create) + * update the zone, create it if necessary, + * and ask for reload or reconfig of bind9 depending on what happened + * @NOTE launched as ROOT + */ + function hook_updatedomains_dns_add($dominfo) { + global $L_FQDN,$L_NS1_HOSTNAME,$L_NS2_HOSTNAME,$L_DEFAULT_MX,$L_DEFAULT_SECONDARY_MX,$L_PUBLIC_IP,$L_PUBLIC_IPV6; + + $domain = $dominfo["domaine"]; + $ttl = $dominfo["zonettl"]; + + // does it already exist? + if (file_exists($this->zone_file_directory."/".$domain)) { + list($islocked,$serial,$more)=$this->read_zone($domain); + $serial++; // only increment serial for new zones + } else { + $more=""; + $serial=date("Ymd")."00"; + $islocked=false; + } + if ($islocked) return; + + // Prepare a new zonefile from a template + $zone = file_get_contents($this->ZONE_TEMPLATE); + // substitute ALTERNC & domain variables + $zone = strtr($zone, array( + "%%fqdn%%" => "$L_FQDN", + "%%ns1%%" => "$L_NS1_HOSTNAME", + "%%ns2%%" => "$L_NS2_HOSTNAME", + "%%DEFAULT_MX%%" => "$L_DEFAULT_MX", + "%%DEFAULT_SECONDARY_MX%%" => "$L_DEFAULT_SECONDARY_MX", + "@@fqdn@@" => "$L_FQDN", + "@@ns1@@" => "$L_NS1_HOSTNAME", + "@@ns2@@" => "$L_NS2_HOSTNAME", + "@@DEFAULT_MX@@" => "$L_DEFAULT_MX", + "@@DEFAULT_SECONDARY_MX@@" => "$L_DEFAULT_SECONDARY_MX", + "@@DOMAINE@@" => $domain, + "@@SERIAL@@" => $serial, + "@@PUBLIC_IP@@" => "$L_PUBLIC_IP", + "@@PUBLIC_IPV6@@" => "$L_PUBLIC_IPV6", + "@@ZONETTL@@" => $ttl, + )); + + // add the SUBDOMAIN entries + $zone .= $this->conf_from_db($domain); + + // add the "END ALTERNC CONF line"; + $zone .= ";;; END ALTERNC AUTOGENERATE CONFIGURATION\n"; + + // add the manually entered info: + $zone .= $more; + file_put_contents($this->zone_file_directory."/".$domain,$zone); + + // add the line into bind9 conf: + if (add_line_to_file( + $this->NAMED_CONF, + trim(strtr( + file_get_contents($this->NAMED_TEMPLATE), + array( + "@@DOMAIN@@" => $DOMAIN, + "@@ZONE_FILE@@" => $this->zone_file_directory."/".$domain + ) + ))) + ) { + $this->shouldreconfig=true; + } else { + $this->shouldreload=true; + } } - // launched for each ZONE for which we want a zone DELETE - function hook_updatedomains_dns_del($domain) { - + + // ------------------------------------------------------------ + /** + * Hook launched for each ZONE for which we want a zone DELETE + * remove the zone and its file, + * and if any action happened, ask for bind RECONFIG at posttime + * @NOTE launched as ROOT + */ + function hook_updatedomains_dns_del($dominfo) { + $domain = $dominfo["domaine"]; + if (remove_line_from_file( + $this->NAMED_CONF, + trim(strtr( + file_get_contents($this->NAMED_TEMPLATE), + array( + "@@DOMAIN@@" => $domain, + "@@ZONE_FILE@@" => $this->zone_file_directory."/".$domain + ) + ))) + ) { + $this->shouldreconfig=true; + } else { + return; + } + @unlink($this->zone_file_directory."/".$domain); } - // launched at the very end of updatedomains + + // ------------------------------------------------------------ + /** + * Hook function launched at the very end of updatedomains + * here, we just reload OR reconfig (or both) bind9 depending + * on what happened before. + * @NOTE launched as ROOT + */ function hook_updatedomains_dns_post() { global $msg; if ($this->shouldreload) { @@ -84,489 +175,63 @@ class m_bind { } } + + // ------------------------------------------------------------ + /** + * read a zone file for $domain, + * @param $domain string the domain name + * @return array with 3 informations: + * is the domain locked? (boolean), what's the current serial (integer), the data after alternc conf (string of lines) + */ + function read_zone($domain) { + $f=fopen($this->zone_file_directory."/".$domain,"rb"); + $islocked=false; + $more=""; + $serial=date("Ymd")."00"; + while ($s=fgets($f,4096)) { + if (preg_match("#\;\s*LOCKED:YES#i",$s)) { + $islocked=true; + } + if (preg_match("/\s*(\d{10})\s+\;\sserial\s?/", $s,$mat)) { + $serial=$mat[1]; + } + if (preg_match('/\;\s*END\sALTERNC\sAUTOGENERATE\sCONFIGURATION(.*)/s', $s)) { + break; + } + } + while ($s=fgets($f,4096)) { + $more.=$s; + } + return array($islocked,$serial,$more); + } + // ------------------------------------------------------------ /** - * Return the part of the conf we got from the database - * + * Return the part of the conf we got from the sub_domaines table * @global m_mysql $db * @param string $domain - * @return array $this->cache_conf_db + * @return string a zonefile excerpt */ - function conf_from_db($domain=false) { + function conf_from_db($domain) { global $db; - // Use cache, fill cache if empty - if (empty($this->cache_conf_db)) { - $db->query(" - select - sd.domaine, - replace(replace(dt.entry,'%TARGET%',sd.valeur), '%SUB%', if(length(sd.sub)>0,sd.sub,'@')) as entry - from + $db->query(" + SELECT + REPLACE(REPLACE(dt.entry,'%TARGET%',sd.valeur), '%SUB%', if(length(sd.sub)>0,sd.sub,'@')) AS ENTRY + FROM sub_domaines sd, domaines_type dt - where + WHERE sd.type=dt.name - and sd.enable in ('ENABLE', 'ENABLED') - order by entry ;"); - $t=array(); - while ($db->next_record()) { - $t[$db->f('domaine')][] = $db->f('entry'); - } - $this->cache_conf_db = $t; + AND sd.enable IN ('ENABLE', 'ENABLED') + ORDER BY ENTRY ;"); + $t=array(); + while ($db->next_record()) { + $t.= $db->f('entry')."\n"; } - if ($domain) { - if (isset($this->cache_conf_db[$domain])) { - return $this->cache_conf_db[$domain]; - } else { - return array(); - } - } // if domain - return $this->cache_conf_db; + return $t; } - - /** - * Return full path of the zone configuration file - * - * @param string $domain - * @return string - */ - function get_zone_file_uri($domain) { - return $this->zone_file_directory.$domain; - } - - - /** - * - * @param string $domain - * @return string zone file path - */ - function get_zone_file($domain) { - // Use cache, fill cache if empty - if (!isset($this->cache_zone_file[$domain]) ) { - if (file_exists($this->get_zone_file_uri($domain))) { - $this->cache_zone_file[$domain] = @file_get_contents($this->get_zone_file_uri($domain)); - } else { - $this->cache_zone_file[$domain] = false; - } - } - return $this->cache_zone_file[$domain] ; - } - - - /** - * - * @param string $domain - * @return string - */ - function get_serial($domain) { - // Return the next serial the domain must have. - // Choose between a generated and an incremented. - - // Calculated : - $calc = date('Ymd').'00'."\n"; - - // Old one : - $old=$calc; // default value - $file = $this->get_zone_file($domain); - preg_match_all("/\s*(\d{10})\s+\;\sserial\s?/", $file, $output_array); - if (isset($output_array[1][0]) && !empty($output_array[1][0])) { - $old = $output_array[1][0]; - } - - // Return max between newly calculated, and old one incremented - return max(array($calc,$old)) + 1 ; - } - - - /** - * Return lines that are after ;;; END ALTERNC AUTOGENERATE CONFIGURATION - * - * @param string $domain - * @return string - */ - function get_persistent($domain) { - if ( ! isset($this->cache_get_persistent[$domain] )) { - preg_match_all('/\;\s*END\sALTERNC\sAUTOGENERATE\sCONFIGURATION(.*)/s', $this->get_zone_file($domain), $output_array); - if (isset($output_array[1][0]) && !empty($output_array[1][0])) { - $this->cache_get_persistent[$domain] = $output_array[1][0]; - } else { - $this->cache_get_persistent[$domain] = false; - } - } // isset - return $this->cache_get_persistent[$domain]; - } - - - /** - * - * @return string - */ - function get_zone_header() { - return file_get_contents($this->ZONE_TEMPLATE); - } - - - /** - * - * @global m_dom $dom - * @param string $domain - * @return array Retourne un tableau - */ - function get_domain_summary($domain=false) { - global $dom; - - // Use cache if is filled, if not, fill it - if (empty($this->cache_domain_summary)) { - $this->cache_domain_summary = $dom->get_domain_all_summary(); - } - - if ($domain) return $this->cache_domain_summary[$domain]; - else return $this->cache_domain_summary; - } - - - /** - * - * @param string $domain - * @return boolean - */ - function dkim_delete($domain) { - $target_dir = "/etc/opendkim/keys/$domain"; - if (file_exists($target_dir)) { - @unlink("$target_dir/alternc_private"); - @unlink("$target_dir/alternc.txt"); - @rmdir($target_dir); - } - return true; - } - - - /** - * Generate the domain DKIM key - * - * @param string $domain - * @return null|boolean - */ - function dkim_generate_key($domain) { - // Stop here if we do not manage the mail - $domainInfo = $this->get_domain_summary($domain); - if ( ! $domainInfo['gesmx'] ) return; - - $target_dir = "/etc/opendkim/keys/$domain"; - - if (file_exists($target_dir.'/alternc.txt')) return; // Do not generate if exist - - if (! is_dir($target_dir)) mkdir($target_dir); // create dir - - // Generate the key - $old_dir=getcwd(); - chdir($target_dir); - exec('opendkim-genkey -r -d '.escapeshellarg($domain).' -s "alternc" '); - chdir($old_dir); - - // opendkim must be owner of the key - chown("$target_dir/alternc.private", 'opendkim'); - chgrp("$target_dir/alternc.private", 'opendkim'); - - return true; // FIXME handle error - } - - - /** - * Refresh DKIM configuration: be sure to list the domain having a private key (and only them) - */ - function dkim_refresh_list() { - // so ugly... but there is only 1 pass, not 3. Still ugly. - $trusted_host_new = "# WARNING: this file is auto generated by AlternC.\n# Add your changes after the last line\n"; - $keytable_new = "# WARNING: this file is auto generated by AlternC.\n# Add your changes after the last line\n"; - $signingtable_new = "# WARNING: this file is auto generated by AlternC.\n# Add your changes after the last line\n"; - - # Generate automatic entry - foreach ($this->get_domain_summary() as $domain => $ds ) { - // Skip if delete in progress, or if we do not manage dns or mail - if ( ! $ds['gesdns'] || ! $ds['gesmx'] || strtoupper($ds['dns_action']) == 'DELETE' ) continue; - - // Skip if there is no key generated - if (! file_exists("/etc/opendkim/keys/$domain/alternc.txt")) continue; - - // Modif the files. - $trusted_host_new.="$domain\n"; - $keytable_new .="alternc._domainkey.$domain $domain:alternc:/etc/opendkim/keys/$domain/alternc.private\n"; - $signingtable_new.="$domain alternc._domainkey.$domain\n"; - } - $trusted_host_new.="# END AUTOMATIC FILE. ADD YOUR CHANGES AFTER THIS LINE\n"; - $keytable_new .="# END AUTOMATIC FILE. ADD YOUR CHANGES AFTER THIS LINE\n"; - $signingtable_new.="# END AUTOMATIC FILE. ADD YOUR CHANGES AFTER THIS LINE\n"; - - # Get old files - $trusted_host_old=@file_get_contents($this->dkim_trusted_host_file); - $keytable_old =@file_get_contents($this->dkim_keytable_file); - $signingtable_old=@file_get_contents($this->dkim_signingtable_file); - - # Keep manuel entry - preg_match_all('/\#\s*END\ AUTOMATIC\ FILE\.\ ADD\ YOUR\ CHANGES\ AFTER\ THIS\ LINE(.*)/s', $trusted_host_old, $output_array); - if (isset($output_array[1][0]) && !empty($output_array[1][0])) { - $trusted_host_new.=$output_array[1][0]; - } - preg_match_all('/\#\s*END\ AUTOMATIC\ FILE\.\ ADD\ YOUR\ CHANGES\ AFTER\ THIS\ LINE(.*)/s', $keytable_old, $output_array); - if (isset($output_array[1][0]) && !empty($output_array[1][0])) { - $keytable_new.=$output_array[1][0]; - } - preg_match_all('/\#\s*END\ AUTOMATIC\ FILE\.\ ADD\ YOUR\ CHANGES\ AFTER\ THIS\ LINE(.*)/s', $signingtable_old, $output_array); - if (isset($output_array[1][0]) && !empty($output_array[1][0])) { - $signingtable_new.=$output_array[1][0]; - } - - // Save if there are some diff - if ( $trusted_host_new != $trusted_host_old ) { - file_put_contents($this->dkim_trusted_host_file, $trusted_host_new); - } - if ( $keytable_new != $keytable_old ) { - file_put_contents($this->dkim_keytable_file, $keytable_new); - } - if ( $signingtable_new != $signingtable_old ) { - file_put_contents($this->dkim_signingtable_file, $signingtable_new); - } - - } - - - /** - * - * @param string $domain - * @return string - */ - function dkim_entry($domain) { - $keyfile="/etc/opendkim/keys/$domain/alternc.txt"; - $domainInfo = $this->get_domain_summary($domain); - if (! file_exists($keyfile) && $domainInfo['gesmx'] ) { - $this->dkim_generate_key($domain); - } - return @file_get_contents($keyfile); - } - - - /** - * Conditionnal generation autoconfig entry for outlook / thunderbird - * If entry with the same name allready exist, skip it. - * - * @param string $domain - * @return string - */ - function mail_autoconfig_entry($domain) { - $zone= implode("\n",$this->conf_from_db($domain))."\n".$this->get_persistent($domain); - - $entry=''; - $domainInfo = $this->get_domain_summary($domain); - if ( $domainInfo['gesmx'] ) { - // If we manage the mail - - // Check if there is no the same entry (defined or manual) - // can be toto IN A or toto.fqdn.tld. IN A - if (! preg_match("/autoconfig(\s|\.".str_replace('.','\.',$domain)."\.)/", $zone )) { - $entry.="autoconfig IN CNAME %%fqdn%%.\n"; - } - if (! preg_match("/autodiscover(\s|\.".str_replace('.','\.',$domain)."\.)/", $zone )) { - $entry.="autodiscover IN CNAME %%fqdn%%.\n"; - } - } // if gesmx - return $entry; - } - - - /** - * - * Return a fully generated zone - * - * @global string $L_FQDN - * @global string $L_NS1_HOSTNAME - * @global string $L_NS2_HOSTNAME - * @global string $L_DEFAULT_MX - * @global string $L_DEFAULT_SECONDARY_MX - * @global string $L_PUBLIC_IP - * @param string $domain - * @return string - */ - function get_zone($domain) { - global $L_FQDN, $L_NS1_HOSTNAME, $L_NS2_HOSTNAME, $L_DEFAULT_MX, $L_DEFAULT_SECONDARY_MX, $L_PUBLIC_IP; - - $zone =$this->get_zone_header(); - $zone.=implode("\n",$this->conf_from_db($domain)); - $zone.="\n;;;HOOKED ENTRY\n"; - - $zone.= $this->dkim_entry($domain); - $zone.= $this->mail_autoconfig_entry($domain); - - $zone.="\n;;; END ALTERNC AUTOGENERATE CONFIGURATION\n"; - $zone.=$this->get_persistent($domain); - $domainInfo = $this->get_domain_summary($domain); - - // FIXME check those vars - $zone = strtr($zone, array( - "%%fqdn%%"=>"$L_FQDN", - "%%ns1%%"=>"$L_NS1_HOSTNAME", - "%%ns2%%"=>"$L_NS2_HOSTNAME", - "%%DEFAULT_MX%%"=>"$L_DEFAULT_MX", - "%%DEFAULT_SECONDARY_MX%%"=>"$L_DEFAULT_SECONDARY_MX", - "@@fqdn@@"=>"$L_FQDN", - "@@ns1@@"=>"$L_NS1_HOSTNAME", - "@@ns2@@"=>"$L_NS2_HOSTNAME", - "@@DEFAULT_MX@@"=>"$L_DEFAULT_MX", - "@@DEFAULT_SECONDARY_MX@@"=>"$L_DEFAULT_SECONDARY_MX", - "@@DOMAINE@@"=>"$domain", - "@@SERIAL@@"=>$this->get_serial($domain), - "@@PUBLIC_IP@@"=>"$L_PUBLIC_IP", - "@@ZONETTL@@"=> $domainInfo['zonettl'], - )); - - return $zone; - } - - - /** - * - * @param string $domain - */ - function reload_zone($domain) { - exec($this->RNDC." reload ".escapeshellarg($domain), $output, $return_value); - if ($return_value != 0 ) { - echo "ERROR: Reload zone failed for zone $domain\n"; - } - } - - - /** - * return true if zone is locked - * - * @param string $domain - * @return boolean - */ - function is_locked($domain) { - preg_match_all("/(\;\s*LOCKED:YES)/i", $this->get_zone_file($domain), $output_array); - if (isset($output_array[1][0]) && !empty($output_array[1][0])) { - return true; - } - return false; - } - - - /** - * - * @global m_mysql $db - * @global m_dom $dom - * @param string $domain - * @return boolean - */ - function save_zone($domain) { - global $db, $dom; - - // Do not save if the zone is LOCKED - if ( $this->is_locked($domain)) { - $dom->set_dns_result($domain, "The zone file of this domain is locked. Contact your administrator."); // If edit, change dummy_for_translation - $dom->set_dns_action($domain, 'OK'); - return false; - } - - // Save file, and apply chmod/chown - $file=$this->get_zone_file_uri($domain); - file_put_contents($file, $this->get_zone($domain)); - chown($file, 'bind'); - chmod($file, 0640); - - $dom->set_dns_action($domain, 'OK'); - return true; // fixme add tests - } - - - /** - * Delete the zone configuration file - * - * @param string $domain - * @return boolean - */ - function delete_zone($domain) { - $file=$this->get_zone_file_uri($domain); - if (file_exists($file)) { - unlink($file); - } - $this->dkim_delete($domain); - return true; - } - - - /** - * - * @global m_hooks $hooks - * @return boolean - */ - function reload_named() { - global $hooks; - // Generate the new conf file - $new_named_conf="// DO NOT EDIT\n// This file is generated by Alternc.\n// Every changes you'll make will be overwrited.\n"; - $tpl=file_get_contents($this->NAMED_TEMPLATE); - foreach ($this->get_domain_summary() as $domain => $ds ) { - if ( ! $ds['gesdns'] || strtoupper($ds['dns_action']) == 'DELETE' ) continue; - $new_named_conf.=strtr($tpl, array("@@DOMAINE@@"=>$domain, "@@ZONE_FILE@@"=>$this->get_zone_file_uri($domain))); - } - - // Get the actual conf file - $old_named_conf = @file_get_contents($this->NAMED_CONF); - - // Apply new configuration only if there are some differences - if ($old_named_conf != $new_named_conf ) { - file_put_contents($this->NAMED_CONF,$new_named_conf); - chown($this->NAMED_CONF, 'bind'); - chmod($this->NAMED_CONF, 0640); - exec($this->RNDC." reconfig"); - $hooks->invoke_scripts("/usr/lib/alternc/reload.d", array('dns_reconfig') ); - } - - return true; - } - - - /** - * Regenerate bind configuration and load it - * - * @global m_hooks $hooks - * @param boolean $all - * @return boolean - */ - function regenerate_conf($all=false) { - global $hooks; - - foreach ($this->get_domain_summary() as $domain => $ds ) { - if ( ! $ds['gesdns'] && strtoupper($ds['dns_action']) == 'OK' ) continue; // Skip if we do not manage DNS and is up-to-date for this domain - - if ( (strtoupper($ds['dns_action']) == 'DELETE' ) || - (strtoupper($ds['dns_action']) == 'UPDATE' && $ds['gesdns']==false ) // in case we update the zone to disable DNS management - ) { - $this->delete_zone($domain); - continue; - } - - if ( ( $all || strtoupper($ds['dns_action']) == 'UPDATE' ) && $ds['gesdns'] ) { - $this->save_zone($domain); - $this->reload_zone($domain); - $hooks->invoke_scripts("/usr/lib/alternc/reload.d", array('dns_reload_zone', $domain) ); - } - } // end foreach domain - - $this->dkim_refresh_list(); - $this->reload_named(); - return true; - } - - - /** - * - */ - private function dummy_for_translation() { - _("The zone file of this domain is locked. Contact your administrator."); - } - - } // m_bind diff --git a/bureau/class/m_dom.php b/bureau/class/m_dom.php index bafab4de..8cabd458 100644 --- a/bureau/class/m_dom.php +++ b/bureau/class/m_dom.php @@ -1442,7 +1442,7 @@ class m_dom { * TRUE sinon. * */ - function edit_domain($dom, $dns, $gesmx, $force = false, $ttl = 86400) { + function edit_domain($dom, $dns, $gesmx, $force = false, $ttl = 3600) { global $db, $msg, $hooks; $msg->log("dom", "edit_domain", $dom . "/" . $dns . "/" . $gesmx); // Locked ? diff --git a/bureau/class/m_mail.php b/bureau/class/m_mail.php index b7f02735..9a370fe7 100644 --- a/bureau/class/m_mail.php +++ b/bureau/class/m_mail.php @@ -469,9 +469,9 @@ ORDER BY if ($db->next_record()) { $db->query("UPDATE sub_domaines SET web_action='DELETE' WHERE domaine= ? AND type='txt' AND (sub='' AND valeur LIKE 'v=spf1 %') OR (sub='_dmarc' AND valeur LIKE 'v=dmarc1;%');", array($db->Record["domaine"])); $db->query("UPDATE sub_domaines SET web_action='DELETE' WHERE domaine= ? AND (type='defmx' OR type='defmx2');", array($db->Record["domaine"])); - $db->query("UPDATE domaines SET dns_action='UPDATE' WHERE id= ? ;", array($dom_id)); + $db->query("UPDATE domaines SET dns_action='UPDATE' WHERE id= ? ;", array($dom_id)); } - + return true; } @@ -912,6 +912,7 @@ ORDER BY } + // ------------------------------------------------------------ /** * hook function called by AlternC when a domain is created for * the current user account using the SLAVE DOMAIN feature @@ -928,6 +929,7 @@ ORDER BY } + // ------------------------------------------------------------ /** * hook function called by AlternC when a domain is created for * the current user account @@ -937,7 +939,7 @@ ORDER BY * @access private */ function hook_dom_add_mx_domain($domain_id) { - global $msg, $mem, $db; + global $msg, $mem, $db, $L_FQDN; $msg->log("mail", "hook_dom_add_mx_domain", $domain_id); $db->query("SELECT value FROM variable where name='mailname_bounce';"); @@ -947,8 +949,9 @@ ORDER BY } $mailname = $db->f("value"); // set spf & dmarc for this domain - $db->query("SELECT domaine FROM domaines WHERE id= ?;", array($domain_id)); + $db->query("SELECT domaine,compte FROM domaines WHERE id= ?;", array($domain_id)); if ($db->next_record()) { + $this->set_dns_autoconf($db->Record["domaine"],$db->Record["compte"]); if ($spf = variable_get("default_spf_value")) { $this->set_dns_spf($db->Record["domaine"], $spf); } @@ -960,13 +963,14 @@ ORDER BY } + // ------------------------------------------------------------ /** * hook function called by variables when a variable is changed * @access private */ function hook_variable_set($name, $old, $new) { global $msg, $db; - $msg->log("mail", "hook_variable_set($name,$old,$new)"); + $msg->log("mail", "hook_variable_set($name,$old,$new)"); if ($name == "default_spf_value") { $new = trim($new); @@ -991,7 +995,34 @@ ORDER BY } } + + // ------------------------------------------------------------ + /** + * Add dns entries for autodiscover / autoconf on the domain + */ + function set_dns_autoconf($domain,$uid=-1) { + global $db, $L_FQDN, $cuid; + $changed=false; + if ($uid==-1) $uid=$cuid; + $db->query("SELECT domaine,sub,type,valeur FROM sub_domaines WHERE domaine=? AND sub='autodiscover' AND type='autodiscover';",array($domain)); + if (!$db->next_record()) { + $db->query("INSERT INTO sub_domaines SET domaine=?, compte=?, sub='autodiscover', type='autodiscover';",array($domain,$uid)); + $changed=true; + } + $db->query("SELECT domaine,sub,type,valeur FROM sub_domaines WHERE domaine=? AND sub='autoconfig' AND type='autodiscover';",array($domain)); + if (!$db->next_record()) { + $db->query("INSERT INTO sub_domaines SET domaine=?, compte=?, sub='autoconfig', type='autodiscover';",array($domain,$uid)); + $changed=true; + } + if ($changed) { + $db->query("UPDATE domaines SET dns_action='UPDATE' WHERE domaine= ?;", array($domain)); + } + return $changed; + } + + + // ------------------------------------------------------------ /** * Set or UPDATE the DNS record for the domain $dom(str) to be $spf * account's login is current and if not it's $login. @@ -1022,6 +1053,7 @@ ORDER BY } + // ------------------------------------------------------------ /** * Set or UPDATE the DNS record for the domain $dom(str) to be $dmarc * account's login is current and if not it's $login. @@ -1055,5 +1087,110 @@ ORDER BY } + /** Manage DKIM when adding / removing a domain MX management */ + var $shouldreloaddkim; + + + // ------------------------------------------------------------ + /** + * Hook launched before doing anything dns-related + */ + function hook_updatedomains_dns_pre() { + global $db; + // for each domain where we don't have the MX or the DNS, remove the DKIM setup + $this->shouldreloaddkim=false; + $db->query("SELECT domaine,gesdns,gesmx FROM domaines WHERE dns_action!='OK';"); + $add=array(); + $del=array(); + while ($db->next_record()) { + if ($db->Record["gesdns"]==0 || $db->Record["gesmx"]==0) { + $del[]=$db->Record["domaine"]; + } else { + $add[]=$db->Record["domaine"]; + } + } + foreach($add as $domain) { + $this->dkim_add($domain); + } + foreach($del as $domain) { + $this->dkim_del($domain); + } + } + + + // ------------------------------------------------------------ + /** + * Hook launched after doing anything dns-related + */ + function hook_updatedomains_dns_post() { + if ($this->shouldreloaddkim) { + exec("service opendkim reload"); + $this->shouldreloaddkim=false; + } + } + + + // ------------------------------------------------------------ + /** + * Add a domain into OpenDKIM configuration + */ + function dkim_add($domain) { + global $db; + $target_dir = "/etc/opendkim/keys/$domain"; + if (file_exists($target_dir.'/alternc.txt')) return; // Do not generate if exist + $this->shouldreloaddkim=true; + if (! is_dir($target_dir)) mkdir($target_dir); // create dir + // Generate the key + $old_dir=getcwd(); + chdir($target_dir); + exec('opendkim-genkey -b 1200 -r -d '.escapeshellarg($domain).' -s "alternc" '); + chdir($old_dir); + // opendkim must be owner of the key + chown("$target_dir/alternc.private", 'opendkim'); + chgrp("$target_dir/alternc.private", 'opendkim'); + + // Add line into files: + add_line_to_file("/etc/opendkim/KeyTable","alternc._domainkey.".$domain." ".$domain.":alternc:/etc/opendkim/keys/".$domain."/alternc.private"); + add_line_to_file("/etc/opendkim/SigningTable",$domain." alternc._domainkey.".$domain); + // Add subdomaine entry + $dkim_key=$this->dkim_get_entry($domain); + $db->query("INSERT INTO sub_domaines SET domaine=?, compte=?, sub='', type='dkim', valeur=?;",array($uid,$domain,$dkim_key)); + // no need to do DNS_ACTION="UPDATE" => we are in the middle of a HOOK, so dns WILL BE reloaded for this domain + } + + + // ------------------------------------------------------------ + /** + * Delete a domain from OpenDKIM configuration + */ + function dkim_del($domain) { + $target_dir = "/etc/opendkim/keys/$domain"; + if (file_exists($target_dir)) { + $this->shouldreloaddkim=true; + @unlink("$target_dir/alternc_private"); + @unlink("$target_dir/alternc.txt"); + @rmdir($target_dir); + del_line_from_file("/etc/opendkim/KeyTable","alternc._domainkey.".$domain." ".$domain.":alternc:/etc/opendkim/keys/".$domain."/alternc.private"); + del_line_from_file("/etc/opendkim/SigningTable",$domain." alternc._domainkey.".$domain); + } + } + + + // ------------------------------------------------------------ + /** + * return the content of the TXT information to be added into the DB for DKIM subdomains + * @param $domain string the name of the domain name + * @return string the TXT entry (without quotes) + * or false if an error occurred + **/ + function dkim_get_entry($domain) { + $key=file_get_contents("/etc/opendkim/keys/".$domain."/alternc.txt"); + if (preg_match('#alternc._domainkey IN TXT "(.*)"#',$key,$mat)) { + return $mat[1]; + } + return false; + } + + // @TODO hook after reloading DNS zones => if necessary, restart opendkim } /* Class m_mail */ diff --git a/install/mysql.sql b/install/mysql.sql index 2e43504c..4fd129f0 100644 --- a/install/mysql.sql +++ b/install/mysql.sql @@ -115,7 +115,7 @@ CREATE TABLE IF NOT EXISTS domaines ( noerase tinyint(4) NOT NULL default '0', dns_action enum ('OK','UPDATE','DELETE') NOT NULL default 'UPDATE', dns_result varchar(255) not null default '', - zonettl int(10) unsigned NOT NULL default '86400', + zonettl int(10) unsigned NOT NULL default '3600', PRIMARY KEY (id), UNIQUE KEY (domaine) ) ENGINE=InnoDB; @@ -479,8 +479,10 @@ CREATE TABLE IF NOT EXISTS `domaines_type` ( PRIMARY KEY ( `name` ) ) ENGINE=InnoDB COMMENT = 'Type of domains allowed'; -INSERT IGNORE INTO `domaines_type` (name, description, target, entry, compatibility, only_dns, need_dns, advanced, enable) values -('vhost', 'Locally hosted', 'DIRECTORY', '%SUB% IN A @@PUBLIC_IP@@', 'txt,defmx,defmx2,mx,mx2', false, false, false, 'ALL'), +INSERT IGNORE INTO `domaines_type` (name, description, target, entry, compatibility, only_dns, need_dns, advanced, enable) VALUES +('dkim', 'DKIM Key', 'NONE', '%SUB% IN TXT "%TARGET%"', 'txt,defmx,defmx2,mx,mx2,url,ip,ipv6', true, true, true, 'ADMIN'), +('autodiscover', 'Autodiscover and autoconf for email', 'NONE', '%SUB% IN A @@PUBLIC_IP@@', 'txt,defmx,defmx2,mx,mx2', false, true, true, 'ADMIN'), + ('vhost', 'Locally hosted', 'DIRECTORY', '%SUB% IN A @@PUBLIC_IP@@', 'txt,defmx,defmx2,mx,mx2', false, false, false, 'ALL'), ('url', 'URL redirection', 'URL', '%SUB% IN A @@PUBLIC_IP@@', 'txt,defmx,defmx2', false, false, false, 'ALL'), ('ip', 'IPv4 redirect', 'IP', '%SUB% IN A %TARGET%', 'url,ip,ipv6,txt,mx,mx2,defmx,defmx2', true, true, false, 'ALL'), ('ipv6', 'IPv6 redirect', 'IPV6', '%SUB% IN AAAA %TARGET%', 'ip,ipv6,txt,mx,mx2,defmx,defmx2', true, true, true, 'ALL'), diff --git a/install/upgrades/3.5.0.1.sql b/install/upgrades/3.5.0.1.sql index 49fc7f44..0cb4a949 100644 --- a/install/upgrades/3.5.0.1.sql +++ b/install/upgrades/3.5.0.1.sql @@ -1,13 +1,22 @@ --- upgrade from 3.4.10 and 3.4.11 (a bug prevented them to be inserted :/ ) +-- migrating DKIM to be inside sub_domaines table +INSERT IGNORE INTO `domaines_type` (name, description, target, entry, compatibility, only_dns, need_dns, advanced, enable) VALUES +('dkim', 'DKIM Key', 'NONE', '%SUB% IN TXT "%TARGET%"', 'txt,defmx,defmx2,mx,mx2,url,ip,ipv6', true, true, true, 'ADMIN'); +-- migrating AUTODISCOVER / AUTOCONF to be inside sub_domaines table +INSERT IGNORE INTO `domaines_type` (name, description, target, entry, compatibility, only_dns, need_dns, advanced, enable) VALUES +('autodiscover', 'Autodiscover and autoconf for email', 'NONE', '%SUB% IN A @@PUBLIC_IP@@', 'txt,defmx,defmx2,mx,mx2', false, true, true, 'ADMIN'); + + +-- upgrade from 3.4.10 and 3.4.11 (a bug prevented them to be inserted :/ ) ALTER TABLE mailbox MODIFY `lastlogin` DATETIME NOT NULL DEFAULT 0; ALTER TABLE mailbox ADD `lastloginsasl` DATETIME NOT NULL DEFAULT 0 AFTER `lastlogin`; +ALTER TABLE `domaines` MODIFY `zonettl` INT(10) UNSIGNED NOT NULL default '3600'; -ALTER TABLE `membres` MODIFY `pass` varchar(255); -ALTER TABLE `ftpusers` MODIFY `encrypted_password` varchar(255); +-- upgrade to better hashes ($6$, 20000 loops) in membres and ftpusers +ALTER TABLE `membres` MODIFY `pass` VARCHAR(255); +ALTER TABLE `ftpusers` MODIFY `encrypted_password` VARCHAR(255); -- upgrade to merge alternc-ssl into alternc + change the way we work on SSL - DROP TABLE IF EXISTS `certif_alias`; ALTER TABLE `certificates` @@ -28,7 +37,8 @@ ALTER TABLE `domaines_type` UPDATE `domaines_type` SET `has_https_option`=1 WHERE name='vhost'; -- Backport old certif_hosts data to sub_domaines -UPDATE `sub_domaines` LEFT JOIN `certif_hosts` ON `sub_domaines`.`id` = `certif_hosts`.`sub` SET `sub_domaines`.`certificate_id` = `certif_hosts`.`certif` WHERE 1; +UPDATE `sub_domaines` LEFT JOIN `certif_hosts` ON `sub_domaines`.`id` = `certif_hosts`.`sub` + SET `sub_domaines`.`certificate_id` = `certif_hosts`.`certif`; DROP TABLE IF EXISTS `certif_hosts`; -- Set https status (http,https,both) @@ -37,21 +47,22 @@ UPDATE `sub_domaines` SET `https` = "both" WHERE `type` LIKE '%-mixssl' AND http UPDATE `sub_domaines` SET `https` = "http" WHERE https = ''; UPDATE `sub_domaines` SET `type` = REPLACE(`type`,'-ssl',''); UPDATE `sub_domaines` SET `type` = REPLACE(`type`,'-mixssl',''); --- Disable https status when domains_type don't provide this -UPDATE `sub_domaines` SET `https` = '' WHERE type IN (select name FROM domaines_type WHERE has_https_option = 0); +-- Disable https status when domains_type don't use it +UPDATE `sub_domaines` SET `https` = '' WHERE type IN (SELECT name FROM domaines_type WHERE has_https_option = 0); --- When two sudomain exists, we consider sub_domains with http and https feature +-- When two subdomain exists, we consider sub_domains with http and https feature UPDATE sub_domaines AS sd INNER JOIN (SELECT MIN(id) id FROM `sub_domaines` GROUP BY domaine,sub,type HAVING count(id) > 1) sd1 ON sd.id = sd1.id SET `https` = "both"; -- Delete duplicate lines -DELETE sd1 FROM sub_domaines sd1, sub_domaines sd2 WHERE sd1.id > sd2.id AND sd1.domaine = sd2.domaine AND sd1.sub = sd2.sub AND sd1.type = sd2.type AND sd1.https <> '' AND sd2.https <> ''; +DELETE sd1 FROM sub_domaines sd1, sub_domaines sd2 + WHERE sd1.id > sd2.id AND sd1.domaine = sd2.domaine AND sd1.sub = sd2.sub AND sd1.type = sd2.type + AND sd1.https <> '' AND sd2.https <> ''; -- we need to regenerate all vhost, they will be by AlternC.install -- UPDATE `sub_domaines` SET `web_action` = 'UPDATE'; - -- change some variable names : UPDATE variable @@ -72,6 +83,6 @@ DELETE FROM variable WHERE name IN ( 'ftp_human_name' ); --- we'd like to prepare IPv6 ;) +-- we'd like to prepare for IPv6 ;) ALTER TABLE `domaines_type` CHANGE `entry` `entry` TEXT DEFAULT ''; diff --git a/install/upgrades/3.5.0.2.php b/install/upgrades/3.5.0.2.php new file mode 100644 index 00000000..4395db26 --- /dev/null +++ b/install/upgrades/3.5.0.2.php @@ -0,0 +1,32 @@ +query("SELECT * FROM domaines WHERE gesdns=1 AND gesmx=1;"); +$add=array(); +while ($db->next_record()) { + $add[$db->Record["domaine"]]=$db->Record["compte"]; +} +foreach($add as $domain => $id) { + // Convert DKIM keys into SUB_DOMAINES table + if (file_exists("/etc/opendkim/keys/".$domain."/alternc.txt")) { + $dkim_key = $mail->dkim_get_entry($domain); + if ($dkim_key) { + // Add subdomain dkim entry + $db->query("INSERT INTO sub_domaines + SET compte=?, domaine=?, sub='@', valeur=?, type='dkim', web_action='OK', web_result=0, enable='ENABLED';", + array($id, $domain, $dkim_key) + ); + // Alternc.INSTALL WILL reload DNS zones anyway, so fear not we don't set dns_action="RELOAD" here. + } + } + // Convert autodiscover into SUB_DOMAINES table + $db->query("INSERT INTO sub_domaines + SET compte=?, domaine=?, sub='@', valeur='', type='autodiscover', web_action='UPDATE', web_result=0, enable='ENABLED';", + array($id, $domain) + ); +} +