Alternc  latest
Alternc logiel libre pour l'hébergement
class_system_bind.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  ----------------------------------------------------------------------
5  LICENSE
6 
7  This program is free software; you can redistribute it and/or
8  modify it under the terms of the GNU General Public License (GPL)
9  as published by the Free Software Foundation; either version 2
10  of the License, or (at your option) any later version.
11 
12  This program is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  GNU General Public License for more details.
16 
17  To read the license please visit http://www.gnu.org/copyleft/gpl.html
18  ----------------------------------------------------------------------
19 */
20 
21 /**
22  * bind9 file management class
23  *
24  * @copyright AlternC-Team 2000-2017 https://alternc.com/
25  */
26 class system_bind {
27  var $ZONE_TEMPLATE ="/etc/alternc/templates/bind/templates/zone.template";
28  var $NAMED_TEMPLATE ="/etc/alternc/templates/bind/templates/named.template";
29  var $NAMED_CONF ="/var/lib/alternc/bind/automatic.conf";
30  var $RNDC ="/usr/sbin/rndc";
31 
32  var $dkim_trusted_host_file = "/etc/opendkim/TrustedHosts";
33  var $dkim_keytable_file = "/etc/opendkim/KeyTable";
34  var $dkim_signingtable_file = "/etc/opendkim/SigningTable";
35 
36  var $cache_conf_db = array();
37  var $cache_get_persistent = array();
38  var $cache_zone_file = array();
39  var $cache_domain_summary = array();
40  var $zone_file_directory = '/var/lib/alternc/bind/zones/';
41 
42 
43  /**
44  * Return the part of the conf we got from the database
45  *
46  * @global m_mysql $db
47  * @param string $domain
48  * @return array $this->cache_conf_db
49  */
50  function conf_from_db($domain=false) {
51  global $db;
52  // Use cache, fill cache if empty
53  if (empty($this->cache_conf_db)) {
54  $db->query("
55  select
56  sd.domaine,
57  replace(replace(dt.entry,'%TARGET%',sd.valeur), '%SUB%', if(length(sd.sub)>0,sd.sub,'@')) as entry
58  from
59  sub_domaines sd,
60  domaines_type dt
61  where
62  sd.type=dt.name
63  and sd.enable in ('ENABLE', 'ENABLED')
64  order by entry ;");
65  $t=array();
66  while ($db->next_record()) {
67  $t[$db->f('domaine')][] = $db->f('entry');
68  }
69  $this->cache_conf_db = $t;
70  }
71  if ($domain) {
72  if (isset($this->cache_conf_db[$domain])) {
73  return $this->cache_conf_db[$domain];
74  } else {
75  return array();
76  }
77  } // if domain
78  return $this->cache_conf_db;
79  }
80 
81 
82  /**
83  * Return full path of the zone configuration file
84  *
85  * @param string $domain
86  * @return string
87  */
89  return $this->zone_file_directory.$domain;
90  }
91 
92 
93  /**
94  *
95  * @param string $domain
96  * @return string zone file path
97  */
98  function get_zone_file($domain) {
99  // Use cache, fill cache if empty
100  if (!isset($this->cache_zone_file[$domain]) ) {
101  if (file_exists($this->get_zone_file_uri($domain))) {
102  $this->cache_zone_file[$domain] = @file_get_contents($this->get_zone_file_uri($domain));
103  } else {
104  $this->cache_zone_file[$domain] = false;
105  }
106  }
107  return $this->cache_zone_file[$domain] ;
108  }
109 
110 
111  /**
112  *
113  * @param string $domain
114  * @return string
115  */
116  function get_serial($domain) {
117  // Return the next serial the domain must have.
118  // Choose between a generated and an incremented.
119 
120  // Calculated :
121  $calc = date('Ymd').'00'."\n";
122 
123  // Old one :
124  $old=$calc; // default value
125  $file = $this->get_zone_file($domain);
126  preg_match_all("/\s*(\d{10})\s+\;\sserial\s?/", $file, $output_array);
127  if (isset($output_array[1][0]) && !empty($output_array[1][0])) {
128  $old = $output_array[1][0];
129  }
130 
131  // Return max between newly calculated, and old one incremented
132  return max(array($calc,$old)) + 1 ;
133  }
134 
135 
136  /**
137  * Return lines that are after ;;; END ALTERNC AUTOGENERATE CONFIGURATION
138  *
139  * @param string $domain
140  * @return string
141  */
143  if ( ! isset($this->cache_get_persistent[$domain] )) {
144  preg_match_all('/\;\s*END\sALTERNC\sAUTOGENERATE\sCONFIGURATION(.*)/s', $this->get_zone_file($domain), $output_array);
145  if (isset($output_array[1][0]) && !empty($output_array[1][0])) {
146  $this->cache_get_persistent[$domain] = $output_array[1][0];
147  } else {
148  $this->cache_get_persistent[$domain] = false;
149  }
150  } // isset
151  return $this->cache_get_persistent[$domain];
152  }
153 
154 
155  /**
156  *
157  * @return string
158  */
159  function get_zone_header() {
160  return file_get_contents($this->ZONE_TEMPLATE);
161  }
162 
163 
164  /**
165  *
166  * @global m_dom $dom
167  * @param string $domain
168  * @return array Retourne un tableau
169  */
170  function get_domain_summary($domain=false) {
171  global $dom;
172 
173  // Use cache if is filled, if not, fill it
174  if (empty($this->cache_domain_summary)) {
175  $this->cache_domain_summary = $dom->get_domain_all_summary();
176  }
177 
178  if ($domain) return $this->cache_domain_summary[$domain];
179  else return $this->cache_domain_summary;
180  }
181 
182 
183  /**
184  *
185  * @param string $domain
186  * @return boolean
187  */
188  function dkim_delete($domain) {
189  $target_dir = "/etc/opendkim/keys/$domain";
190  if (file_exists($target_dir)) {
191  @unlink("$target_dir/alternc_private");
192  @unlink("$target_dir/alternc.txt");
193  @rmdir($target_dir);
194  }
195  return true;
196  }
197 
198 
199  /**
200  * Generate the domain DKIM key
201  *
202  * @param string $domain
203  * @return null|boolean
204  */
206  // Stop here if we do not manage the mail
207  $domainInfo = $this->get_domain_summary($domain);
208  if ( ! $domainInfo['gesmx'] ) return;
209 
210  $target_dir = "/etc/opendkim/keys/$domain";
211 
212  if (file_exists($target_dir.'/alternc.txt')) return; // Do not generate if exist
213 
214  if (! is_dir($target_dir)) mkdir($target_dir); // create dir
215 
216  // Generate the key
217  $old_dir=getcwd();
218  chdir($target_dir);
219  exec('opendkim-genkey -r -d '.escapeshellarg($domain).' -s "alternc" ');
220  chdir($old_dir);
221 
222  // opendkim must be owner of the key
223  chown("$target_dir/alternc.private", 'opendkim');
224  chgrp("$target_dir/alternc.private", 'opendkim');
225 
226  return true; // FIXME handle error
227  }
228 
229 
230  /**
231  * Refresh DKIM configuration: be sure to list the domain having a private key (and only them)
232  */
233  function dkim_refresh_list() {
234  // so ugly... but there is only 1 pass, not 3. Still ugly.
235  $trusted_host_new = "# WARNING: this file is auto generated by AlternC.\n# Add your changes after the last line\n";
236  $keytable_new = "# WARNING: this file is auto generated by AlternC.\n# Add your changes after the last line\n";
237  $signingtable_new = "# WARNING: this file is auto generated by AlternC.\n# Add your changes after the last line\n";
238 
239  # Generate automatic entry
240  foreach ($this->get_domain_summary() as $domain => $ds ) {
241  // Skip if delete in progress, or if we do not manage dns or mail
242  if ( ! $ds['gesdns'] || ! $ds['gesmx'] || strtoupper($ds['dns_action']) == 'DELETE' ) continue;
243 
244  // Skip if there is no key generated
245  if (! file_exists("/etc/opendkim/keys/$domain/alternc.txt")) continue;
246 
247  // Modif the files.
248  $trusted_host_new.="$domain\n";
249  $keytable_new .="alternc._domainkey.$domain $domain:alternc:/etc/opendkim/keys/$domain/alternc.private\n";
250  $signingtable_new.="$domain alternc._domainkey.$domain\n";
251  }
252  $trusted_host_new.="# END AUTOMATIC FILE. ADD YOUR CHANGES AFTER THIS LINE\n";
253  $keytable_new .="# END AUTOMATIC FILE. ADD YOUR CHANGES AFTER THIS LINE\n";
254  $signingtable_new.="# END AUTOMATIC FILE. ADD YOUR CHANGES AFTER THIS LINE\n";
255 
256  # Get old files
257  $trusted_host_old=@file_get_contents($this->dkim_trusted_host_file);
258  $keytable_old =@file_get_contents($this->dkim_keytable_file);
259  $signingtable_old=@file_get_contents($this->dkim_signingtable_file);
260 
261  # Keep manuel entry
262  preg_match_all('/\#\s*END\ AUTOMATIC\ FILE\.\ ADD\ YOUR\ CHANGES\ AFTER\ THIS\ LINE(.*)/s', $trusted_host_old, $output_array);
263  if (isset($output_array[1][0]) && !empty($output_array[1][0])) {
264  $trusted_host_new.=$output_array[1][0];
265  }
266  preg_match_all('/\#\s*END\ AUTOMATIC\ FILE\.\ ADD\ YOUR\ CHANGES\ AFTER\ THIS\ LINE(.*)/s', $keytable_old, $output_array);
267  if (isset($output_array[1][0]) && !empty($output_array[1][0])) {
268  $keytable_new.=$output_array[1][0];
269  }
270  preg_match_all('/\#\s*END\ AUTOMATIC\ FILE\.\ ADD\ YOUR\ CHANGES\ AFTER\ THIS\ LINE(.*)/s', $signingtable_old, $output_array);
271  if (isset($output_array[1][0]) && !empty($output_array[1][0])) {
272  $signingtable_new.=$output_array[1][0];
273  }
274 
275  // Save if there are some diff
276  if ( $trusted_host_new != $trusted_host_old ) {
277  file_put_contents($this->dkim_trusted_host_file, $trusted_host_new);
278  }
279  if ( $keytable_new != $keytable_old ) {
280  file_put_contents($this->dkim_keytable_file, $keytable_new);
281  }
282  if ( $signingtable_new != $signingtable_old ) {
283  file_put_contents($this->dkim_signingtable_file, $signingtable_new);
284  }
285 
286  }
287 
288 
289  /**
290  *
291  * @param string $domain
292  * @return string
293  */
294  function dkim_entry($domain) {
295  $keyfile="/etc/opendkim/keys/$domain/alternc.txt";
296  $domainInfo = $this->get_domain_summary($domain);
297  if (! file_exists($keyfile) && $domainInfo['gesmx'] ) {
298  $this->dkim_generate_key($domain);
299  }
300  return @file_get_contents($keyfile);
301  }
302 
303 
304  /**
305  * Conditionnal generation autoconfig entry for outlook / thunderbird
306  * If entry with the same name allready exist, skip it.
307  *
308  * @param string $domain
309  * @return string
310  */
312  $zone= implode("\n",$this->conf_from_db($domain))."\n".$this->get_persistent($domain);
313 
314  $entry='';
315  $domainInfo = $this->get_domain_summary($domain);
316  if ( $domainInfo['gesmx'] ) {
317  // If we manage the mail
318 
319  // Check if there is no the same entry (defined or manual)
320  // can be toto IN A or toto.fqdn.tld. IN A
321  if (! preg_match("/autoconfig(\s|\.".str_replace('.','\.',$domain)."\.)/", $zone )) {
322  $entry.="autoconfig IN CNAME %%fqdn%%.\n";
323  }
324  if (! preg_match("/autodiscover(\s|\.".str_replace('.','\.',$domain)."\.)/", $zone )) {
325  $entry.="autodiscover IN CNAME %%fqdn%%.\n";
326  }
327  } // if gesmx
328  return $entry;
329  }
330 
331 
332  /**
333  *
334  * Return a fully generated zone
335  *
336  * @global string $L_FQDN
337  * @global string $L_NS1_HOSTNAME
338  * @global string $L_NS2_HOSTNAME
339  * @global string $L_DEFAULT_MX
340  * @global string $L_DEFAULT_SECONDARY_MX
341  * @global string $L_PUBLIC_IP
342  * @param string $domain
343  * @return string
344  */
345  function get_zone($domain) {
346  global $L_FQDN, $L_NS1_HOSTNAME, $L_NS2_HOSTNAME, $L_DEFAULT_MX, $L_DEFAULT_SECONDARY_MX, $L_PUBLIC_IP;
347 
348  $zone =$this->get_zone_header();
349  $zone.=implode("\n",$this->conf_from_db($domain));
350  $zone.="\n;;;HOOKED ENTRY\n";
351 
352  $zone.= $this->dkim_entry($domain);
353  $zone.= $this->mail_autoconfig_entry($domain);
354 
355  $zone.="\n;;; END ALTERNC AUTOGENERATE CONFIGURATION\n";
356  $zone.=$this->get_persistent($domain);
357  $domainInfo = $this->get_domain_summary($domain);
358 
359  // FIXME check those vars
360  $zone = strtr($zone, array(
361  "%%fqdn%%"=>"$L_FQDN",
362  "%%ns1%%"=>"$L_NS1_HOSTNAME",
363  "%%ns2%%"=>"$L_NS2_HOSTNAME",
364  "%%DEFAULT_MX%%"=>"$L_DEFAULT_MX",
365  "%%DEFAULT_SECONDARY_MX%%"=>"$L_DEFAULT_SECONDARY_MX",
366  "@@fqdn@@"=>"$L_FQDN",
367  "@@ns1@@"=>"$L_NS1_HOSTNAME",
368  "@@ns2@@"=>"$L_NS2_HOSTNAME",
369  "@@DEFAULT_MX@@"=>"$L_DEFAULT_MX",
370  "@@DEFAULT_SECONDARY_MX@@"=>"$L_DEFAULT_SECONDARY_MX",
371  "@@DOMAINE@@"=>"$domain",
372  "@@SERIAL@@"=>$this->get_serial($domain),
373  "@@PUBLIC_IP@@"=>"$L_PUBLIC_IP",
374  "@@ZONETTL@@"=> $domainInfo['zonettl'],
375  ));
376 
377  return $zone;
378  }
379 
380 
381  /**
382  *
383  * @param string $domain
384  */
385  function reload_zone($domain) {
386  exec($this->RNDC." reload ".escapeshellarg($domain), $output, $return_value);
387  if ($return_value != 0 ) {
388  echo "ERROR: Reload zone failed for zone $domain\n";
389  }
390  }
391 
392 
393  /**
394  * return true if zone is locked
395  *
396  * @param string $domain
397  * @return boolean
398  */
399  function is_locked($domain) {
400  preg_match_all("/(\;\s*LOCKED:YES)/i", $this->get_zone_file($domain), $output_array);
401  if (isset($output_array[1][0]) && !empty($output_array[1][0])) {
402  return true;
403  }
404  return false;
405  }
406 
407 
408  /**
409  *
410  * @global m_mysql $db
411  * @global m_dom $dom
412  * @param string $domain
413  * @return boolean
414  */
415  function save_zone($domain) {
416  global $db, $dom;
417 
418  // Do not save if the zone is LOCKED
419  if ( $this->is_locked($domain)) {
420  $dom->set_dns_result($domain, "The zone file of this domain is locked. Contact your administrator."); // If edit, change dummy_for_translation
421  $dom->set_dns_action($domain, 'OK');
422  return false;
423  }
424 
425  // Save file, and apply chmod/chown
426  $file=$this->get_zone_file_uri($domain);
427  file_put_contents($file, $this->get_zone($domain));
428  chown($file, 'bind');
429  chmod($file, 0640);
430 
431  $dom->set_dns_action($domain, 'OK');
432  return true; // fixme add tests
433  }
434 
435 
436  /**
437  * Delete the zone configuration file
438  *
439  * @param string $domain
440  * @return boolean
441  */
442  function delete_zone($domain) {
443  $file=$this->get_zone_file_uri($domain);
444  if (file_exists($file)) {
445  unlink($file);
446  }
447  $this->dkim_delete($domain);
448  return true;
449  }
450 
451 
452  /**
453  *
454  * @global m_hooks $hooks
455  * @return boolean
456  */
457  function reload_named() {
458  global $hooks;
459  // Generate the new conf file
460  $new_named_conf="// DO NOT EDIT\n// This file is generated by Alternc.\n// Every changes you'll make will be overwrited.\n";
461  $tpl=file_get_contents($this->NAMED_TEMPLATE);
462  foreach ($this->get_domain_summary() as $domain => $ds ) {
463  if ( ! $ds['gesdns'] || strtoupper($ds['dns_action']) == 'DELETE' ) continue;
464  $new_named_conf.=strtr($tpl, array("@@DOMAINE@@"=>$domain, "@@ZONE_FILE@@"=>$this->get_zone_file_uri($domain)));
465  }
466 
467  // Get the actual conf file
468  $old_named_conf = @file_get_contents($this->NAMED_CONF);
469 
470  // Apply new configuration only if there are some differences
471  if ($old_named_conf != $new_named_conf ) {
472  file_put_contents($this->NAMED_CONF,$new_named_conf);
473  chown($this->NAMED_CONF, 'bind');
474  chmod($this->NAMED_CONF, 0640);
475  exec($this->RNDC." reconfig");
476  $hooks->invoke_scripts("/usr/lib/alternc/reload.d", array('dns_reconfig') );
477  }
478 
479  return true;
480  }
481 
482 
483  /**
484  * Regenerate bind configuration and load it
485  *
486  * @global m_hooks $hooks
487  * @param boolean $all
488  * @return boolean
489  */
490  function regenerate_conf($all=false) {
491  global $hooks;
492 
493  foreach ($this->get_domain_summary() as $domain => $ds ) {
494  if ( ! $ds['gesdns'] && strtoupper($ds['dns_action']) == 'OK' ) continue; // Skip if we do not manage DNS and is up-to-date for this domain
495 
496  if ( (strtoupper($ds['dns_action']) == 'DELETE' ) ||
497  (strtoupper($ds['dns_action']) == 'UPDATE' && $ds['gesdns']==false ) // in case we update the zone to disable DNS management
498  ) {
499  $this->delete_zone($domain);
500  continue;
501  }
502 
503  if ( ( $all || strtoupper($ds['dns_action']) == 'UPDATE' ) && $ds['gesdns'] ) {
504  $this->save_zone($domain);
505  $this->reload_zone($domain);
506  $hooks->invoke_scripts("/usr/lib/alternc/reload.d", array('dns_reload_zone', $domain) );
507  }
508  } // end foreach domain
509 
510  $this->dkim_refresh_list();
511  $this->reload_named();
512  return true;
513  }
514 
515 
516  /**
517  *
518  */
519  private function dummy_for_translation() {
520  _("The zone file of this domain is locked. Contact your administrator.");
521  }
522 
523 
524 } /* Class system_bind */
525 
$hooks
Definition: bootstrap.php:75
reload_zone($domain)
dkim_generate_key($domain)
Generate the domain DKIM key.
get_domain_summary($domain=false)
delete_zone($domain)
Delete the zone configuration file.
global $db
Definition: bootstrap.php:22
get_zone_file_uri($domain)
Return full path of the zone configuration file.
get_zone_file($domain)
foreach($domaines_user as $domaine) $t
mail_autoconfig_entry($domain)
Conditionnal generation autoconfig entry for outlook / thunderbird If entry with the same name allrea...
bind9 file management class
dkim_delete($domain)
is_locked($domain)
return true if zone is locked
get_persistent($domain)
Return lines that are after ;;; END ALTERNC AUTOGENERATE CONFIGURATION.
$domain
Definition: dom_import.php:36
$dom
Definition: whois_test.php:10
conf_from_db($domain=false)
Return the part of the conf we got from the database.
dkim_refresh_list()
Refresh DKIM configuration: be sure to list the domain having a private key (and only them) ...
regenerate_conf($all=false)
Regenerate bind configuration and load it.
get_zone($domain)
Return a fully generated zone.