1<?php
2
3/**
4 *
5 * bareos-webui - Bareos Web-Frontend
6 *
7 * @link      https://github.com/bareos/bareos for the canonical source repository
8 * @copyright Copyright (c) 2014-2021 Bareos GmbH & Co. KG
9 * @license   GNU Affero General Public License (http://www.gnu.org/licenses/)
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Affero General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * This program is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 * GNU Affero General Public License for more details.
20 *
21 * You should have received a copy of the GNU Affero General Public License
22 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
23 *
24 */
25
26namespace Bareos\BSock;
27
28class BareosBSock implements BareosBSockInterface
29{
30   const BNET_TLS_NONE = 0;   /* cannot do TLS */
31   const BNET_TLS_OK = 1;      /* can do, but not required on my end */
32   const BNET_TLS_REQUIRED = 2;   /* TLS is required */
33
34   const BNET_EOD = -1;      /* End of data stream, new data may follow */
35   const BNET_EOD_POLL = -2;   /* End of data and poll all in one */
36   const BNET_STATUS = -3;      /* Send full status */
37   const BNET_TERMINATE = -4;   /* Conversation terminated, doing close() */
38   const BNET_POLL = -5;      /* Poll request, I'm hanging on a read */
39   const BNET_HEARTBEAT = -6;   /* Heartbeat Response requested */
40   const BNET_HB_RESPONSE = -7;   /* Only response permited to HB */
41   const BNET_xxxxxxPROMPT = -8;   /* No longer used -- Prompt for subcommand */
42   const BNET_BTIME = -9;      /* Send UTC btime */
43   const BNET_BREAK = -10;      /* Stop current command -- ctl-c */
44   const BNET_START_SELECT = -11;   /* Start of a selection list */
45   const BNET_END_SELECT = -12;   /* End of a select list */
46   const BNET_INVALID_CMD = -13;   /* Invalid command sent */
47   const BNET_CMD_FAILED = -14;   /* Command failed */
48   const BNET_CMD_OK = -15;   /* Command succeeded */
49   const BNET_CMD_BEGIN = -16;   /* Start command execution */
50   const BNET_MSGS_PENDING = -17;   /* Messages pending */
51   const BNET_MAIN_PROMPT = -18;   /* Server ready and waiting */
52   const BNET_SELECT_INPUT = -19;   /* Return selection input */
53   const BNET_WARNING_MSG = -20;   /* Warning message */
54   const BNET_ERROR_MSG = -21;   /* Error message -- command failed */
55   const BNET_INFO_MSG = -22;   /* Info message -- status line */
56   const BNET_RUN_CMD = -23;   /* Run command follows */
57   const BNET_YESNO = -24;      /* Request yes no response */
58   const BNET_START_RTREE = -25;   /* Start restore tree mode */
59   const BNET_END_RTREE = -26;   /* End restore tree mode */
60   const BNET_SUB_PROMPT = -27;   /* Indicate we are at a subprompt */
61   const BNET_TEXT_INPUT = -28;   /* Get text input from user */
62
63   const DIR_OK_AUTH = "1000 OK auth\n";
64   const DIR_AUTH_FAILED = "1999 Authorization failed.\n";
65
66   protected $config = array(
67      'debug' => false,
68      'host' => null,
69      'port' => null,
70      'password' => null,
71      'console_name' => null,
72      'pam_password' => null,
73      'pam_username' => null,
74      'tls_verify_peer' => null,
75      'server_can_do_tls' => null,
76      'server_requires_tls' => null,
77      'client_can_do_tls' => null,
78      'client_requires_tls' => null,
79      'ca_file' => null,
80      'cert_file' => null,
81      'cert_file_passphrase' => null,
82      'allowed_cns' => null,
83      'catalog' => null,
84   );
85
86   private $socket = null;
87
88   /**
89    * Setter for testing purposes
90    */
91   public function set_config_param($key, $value)
92   {
93      $this->config[$key] = $value;
94   }
95
96   /**
97    * Authenticate
98    */
99   public function connect_and_authenticate()
100   {
101      if(self::connect()) {
102         return true;
103      } else {
104         return false;
105      }
106   }
107
108   /**
109    * Set configuration
110    */
111   private function set_config_keyword($setting, $key)
112   {
113      if (array_key_exists($key, $this->config)) {
114         $this->config[$key] = $setting;
115      } else {
116         throw new \Exception("Illegal parameter $key in /config/autoload/local.php");
117      }
118   }
119
120   /**
121    * Set user credentials
122    */
123   public function set_user_credentials($username, $password)
124   {
125      if(!$this->config['console_name']) {
126        $this->config['console_name'] = $username;
127        $this->config['password'] = $password;
128      }
129
130      $this->config['pam_password'] = $password;
131      $this->config['pam_username'] = $username;
132
133      if($this->config['debug']) {
134         // extended debug: print config array
135         var_dump($this->config);
136      }
137   }
138
139   /**
140    * Set the connection configuration
141    *
142    * @param $config
143    */
144   public function set_config($config)
145   {
146      array_walk($config, array('self', 'set_config_keyword'));
147
148      if($this->config['debug']) {
149         // extended debug: print config array
150         var_dump($this->config);
151      }
152
153      if(!empty($config['console_name'])) {
154        $this->config['console_name'] = $config['console_name'];
155      }
156      if(!empty($config['console_password'])) {
157        $this->config['password'] = $config['console_password'];
158      }
159   }
160
161   /**
162    * Network to host length
163    *
164    * @param $buffer
165    * @return int
166    */
167   private function ntohl($buffer)
168   {
169      $len = array();
170      $actual_length = 0;
171
172      $len = unpack('N', $buffer);
173      $actual_length = (float) $len[1];
174
175      if($actual_length > (float)2147483647) {
176         $actual_length -= (float)"4294967296";
177      }
178
179      return (int) $actual_length;
180   }
181
182   /**
183    * Replace spaces in a string with the special escape character ^A which is used
184    * to send strings with spaces to specific director commands.
185    *
186    * @param $str
187    * @return string
188    */
189   private function bash_spaces($str)
190   {
191      $length = strlen($str);
192      $bashed_str = "";
193
194      for($i = 0; $i < $length; $i++) {
195         if($str[$i] == ' ') {
196            $bashed_str .= '^A';
197         } else {
198            $bashed_str .= $str[$i];
199         }
200      }
201
202      return $bashed_str;
203   }
204
205   /**
206    * Send a string over the console socket.
207    * Encode the length as the first 4 bytes of the message and append the string.
208    *
209    * @param $msg
210    * @return boolean
211    */
212   private function send($msg)
213   {
214      $str_length = 0;
215      $str_length = strlen($msg);
216      $msg = pack('N', $str_length) . $msg;
217      $str_length += 4;
218      while($this->socket && $str_length > 0) {
219         $send = fwrite($this->socket, $msg, $str_length);
220         if($send === 0 || $send === false) {
221            fclose($this->socket);
222            $this->socket = null;
223            return false;
224         } elseif($send < $str_length) {
225            $msg = substr($msg, $send);
226            $str_length -= $send;
227         } else {
228            return true;
229         }
230      }
231      return false;
232   }
233
234   /**
235    * Receive a string over the console socket.
236    * First read first 4 bytes which encoded the length of the string and
237    * the read the actual string.
238    *
239    * @return string
240    */
241   private function receive($len=0)
242   {
243      $buffer = "";
244      $msg_len = 0;
245
246      if (!$this->socket) {
247        return $buffer;
248      }
249
250      if ($len === 0) {
251         $buffer = stream_get_contents($this->socket, 4);
252         if($buffer == false){
253            return false;
254         }
255         $msg_len = self::ntohl($buffer);
256      } else {
257         $msg_len = $len;
258      }
259
260      if ($msg_len > 0) {
261         $buffer = stream_get_contents($this->socket, $msg_len);
262      }
263
264      return $buffer;
265   }
266
267   /**
268    * Special receive function that also knows the different so called BNET signals the
269    * Bareos director can send as part of the data stream.
270    *
271    * @return string
272    */
273   private function receive_message()
274   {
275      $msg = "";
276      $buffer = "";
277
278      if (!$this->socket) {
279        return $msg;
280      }
281
282      while (true) {
283         $buffer = stream_get_contents($this->socket, 4);
284
285         if ($buffer === false) {
286            throw new \Exception("Error reading socket. " . socket_strerror(socket_last_error()) . "\n");
287         }
288
289         $len = self::ntohl($buffer);
290
291         if ($len === 0) {
292            break;
293         }
294         if ($len > 0) {
295            $msg .= stream_get_contents($this->socket, $len);
296         } elseif ($len < 0) {
297            // signal received
298            switch ($len) {
299               case self::BNET_EOD:
300                  if ($this->config['debug']) {
301                     echo "Got BNET_EOD\n";
302                  }
303                  return $msg;
304               case self::BNET_EOD_POLL:
305                  if ($this->config['debug']) {
306                     echo "Got BNET_EOD_POLL\n";
307                  }
308                  break;
309               case self::BNET_STATUS:
310                  if ($this->config['debug']) {
311                     echo "Got BNET_STATUS\n";
312                  }
313                  break;
314               case self::BNET_TERMINATE:
315                  if ($this->config['debug']) {
316                     echo "Got BNET_TERMINATE\n";
317                  }
318                  break;
319               case self::BNET_POLL:
320                  if ($this->config['debug']) {
321                     echo "Got BNET_POLL\n";
322                  }
323                  break;
324               case self::BNET_HEARTBEAT:
325                  if ($this->config['debug']) {
326                     echo "Got BNET_HEARTBEAT\n";
327                  }
328                  break;
329               case self::BNET_HB_RESPONSE:
330                  if ($this->config['debug']) {
331                     echo "Got BNET_HB_RESPONSE\n";
332                  }
333                  break;
334               case self::BNET_xxxxxxPROMPT:
335                  if ($this->config['debug']) {
336                     echo "Got BNET_xxxxxxPROMPT\n";
337                  }
338                  break;
339               case self::BNET_BTIME:
340                  if ($this->config['debug']) {
341                     echo "Got BNET_BTIME\n";
342                  }
343                  break;
344               case self::BNET_BREAK:
345                  if ($this->config['debug']) {
346                     echo "Got BNET_BREAK\n";
347                  }
348                  break;
349               case self::BNET_START_SELECT:
350                  if ($this->config['debug']) {
351                     echo "Got BNET_START_SELECT\n";
352                  }
353                  break;
354               case self::BNET_END_SELECT:
355                  if ($this->config['debug']) {
356                     echo "Got BNET_END_SELECT\n";
357                  }
358                  break;
359               case self::BNET_INVALID_CMD:
360                  if ($this->config['debug']) {
361                     echo "Got BNET_INVALID_CMD\n";
362                  }
363                  break;
364               case self::BNET_CMD_FAILED:
365                  if ($this->config['debug']) {
366                     echo "Got BNET_CMD_FAILED\n";
367                  }
368                  break;
369               case self::BNET_CMD_OK:
370                  if ($this->config['debug']) {
371                     echo "Got BNET_CMD_OK\n";
372                  }
373                  break;
374               case self::BNET_CMD_BEGIN:
375                  if ($this->config['debug']) {
376                     echo "Got BNET_CMD_BEGIN\n";
377                  }
378                  break;
379               case self::BNET_MSGS_PENDING:
380                  if ($this->config['debug']) {
381                     echo "Got BNET_MSGS_PENDING\n";
382                  }
383                  break;
384               case self::BNET_MAIN_PROMPT:
385                  if ($this->config['debug']) {
386                     echo "Got BNET_MAIN_PROMPT\n";
387                  }
388                  return $msg;
389               case self::BNET_SELECT_INPUT:
390                  if ($this->config['debug']) {
391                     echo "Got BNET_SELECT_INPUT\n";
392                  }
393                  break;
394               case self::BNET_WARNING_MSG:
395                  if ($this->config['debug']) {
396                     echo "Got BNET_WARNINGS_MSG\n";
397                  }
398                  break;
399               case self::BNET_ERROR_MSG:
400                  if ($this->config['debug']) {
401                     echo "Got BNET_ERROR_MSG\n";
402                  }
403                  break;
404               case self::BNET_INFO_MSG:
405                  if ($this->config['debug']) {
406                     echo "Got BNET_INFO_MSG\n";
407                  }
408                  break;
409               case self::BNET_RUN_CMD:
410                  if ($this->config['debug']) {
411                     echo "Got BNET_RUN_CMD\n";
412                  }
413                  break;
414               case self::BNET_YESNO:
415                  if ($this->config['debug']) {
416                     echo "Got BNET_YESNO\n";
417                  }
418                  break;
419               case self::BNET_START_RTREE:
420                  if ($this->config['debug']) {
421                     echo "Got BNET_START_RTREE\n";
422                  }
423                  break;
424               case self::BNET_END_RTREE:
425                  if ($this->config['debug']) {
426                     echo "Got BNET_END_RTREE\n";
427                  }
428                  break;
429               case self::BNET_SUB_PROMPT:
430                  if ($this->config['debug']) {
431                     echo "Got BNET_SUB_PROMPT\n";
432                  }
433                  return $msg;
434               case self::BNET_TEXT_INPUT:
435                  if ($this->config['debug']) {
436                     echo "Got BNET_TEXT_INPUT\n";
437                  }
438                  break;
439               default:
440                  throw new \Exception("Received unknown signal " . $len . "\n");
441                  break;
442            }
443         } else {
444            throw new \Exception("Received illegal packet of size " . $len . "\n");
445         }
446      }
447
448      return $msg;
449   }
450
451
452   /**
453    * Connect to a Bareos Director, authenticate the session and establish TLS if needed.
454    *
455    * @return boolean
456    */
457   private function connect()
458   {
459      if (!isset($this->config['host']) or !isset($this->config['port'])) {
460         return false;
461      }
462
463      if($this->config['debug']) {
464         error_log("console_name: ".$this->config['console_name']);
465      }
466
467      $port = $this->config['port'];
468      $remote = "tcp://" . $this->config['host'] . ":" . $port;
469
470      // set stream context options
471      $opts = array();
472
473      // create stream context
474      $context = stream_context_create($opts);
475
476      try {
477         //$this->socket = stream_socket_client($remote, $error, $errstr, 60, STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT, $context);
478         $this->socket = stream_socket_client($remote, $error, $errstr, 60, STREAM_CLIENT_CONNECT, $context);
479
480         if (!$this->socket) {
481            throw new \Exception("Error: " . $errstr . ", director seems to be down or blocking our request.");
482         }
483         // socket_set_nonblock($this->socket);
484      }
485      catch(\Exception $e) {
486         echo $e->getMessage();
487         exit;
488      }
489      if($this->config['debug']) {
490         echo "Connected to " . $this->config['host'] . " on port " . $this->config['port'] . "\n";
491      }
492
493      /*
494       * It only makes sense to setup the whole TLS context when we as client support or
495       * demand a TLS connection.
496       */
497      if ($this->config['client_can_do_tls'] || $this->config['client_requires_tls']) {
498         /*
499          * We verify the peer ourself so the normal stream layer doesn't need to.
500          * But that does mean we need to capture the certficate.
501          */
502         $result = stream_context_set_option($context, 'ssl', 'verify_peer', false);
503         $result = stream_context_set_option($context, 'ssl', 'verify_peer_name', false);
504         $result = stream_context_set_option($context, 'ssl', 'capture_peer_cert', true);
505
506         /*
507          * Setup a CA file
508          */
509         if (!empty($this->config['ca_file'])) {
510            $result = stream_context_set_option($context, 'ssl', 'cafile', $this->config['ca_file']);
511            if ($this->config['tls_verify_peer']) {
512               $result = stream_context_set_option($context, 'ssl', 'verify_peer', true);
513               $result = stream_context_set_option($context, 'ssl', 'verify_peer_name', true);
514            }
515         } else {
516            $result = stream_context_set_option($context, 'ssl', 'allow_self_signed', true);
517         }
518
519         /*
520          * Cert file which needs to contain the client certificate and the key in PEM encoding.
521          */
522         if (!empty($this->config['cert_file'])) {
523            $result = stream_context_set_option($context, 'ssl', 'local_cert', $this->config['cert_file']);
524
525            /*
526             * Passphrase needed to unlock the above cert file.
527             */
528            if (!empty($this->config['cert_file_passphrase'])) {
529               $result = stream_context_set_option($context, 'ssl', 'passphrase', $this->config['cert_file_passphrase']);
530            }
531         }
532      }
533
534      if (($this->config['server_can_do_tls'] || $this->config['server_requires_tls']) &&
535         ($this->config['client_can_do_tls'] || $this->config['client_requires_tls'])) {
536
537        /*
538         * STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT was introduced with PHP version
539         * 5.6.0. We need to care that calling stream_socket_enable_crypto method
540         * works with versions < 5.6.0 as well.
541         */
542         $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
543
544         if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
545            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
546         }
547
548         $result = stream_socket_enable_crypto($this->socket, true, $crypto_method);
549
550         if (!$result) {
551            throw new \Exception("Error in TLS handshake\n");
552         }
553
554         if ($this->config['tls_verify_peer']) {
555            if (!empty($this->config['allowed_cns'])) {
556               if (!self::tls_postconnect_verify_cn()) {
557                  throw new \Exception("Error in TLS postconnect verify CN\n");
558               }
559            } else {
560               if (!self::tls_postconnect_verify_host()) {
561                  throw new \Exception("Error in TLS postconnect verify host\n");
562               }
563            }
564         }
565      }
566
567      if (!self::login()) {
568         return false;
569      }
570
571      $recv = self::receive();
572      if($this->config['debug']) {
573         error_log($recv);
574      }
575
576      if (!strncasecmp($recv, "1001", 4)) {
577          $pam_answer = "4002".chr(0x1e).$this->config['pam_username'].chr(0x1e).$this->config['pam_password'];
578          if (!self::send($pam_answer)) {
579            error_log("Send failed for pam credentials");
580            return false;
581          }
582          $recv = self::receive();
583          if($this->config['debug']) {
584             error_log($recv);
585          }
586      }
587
588      if (!strncasecmp($recv, "1000", 4)) {
589         $recv = self::receive();
590         if($this->config['debug']) {
591            error_log($recv);
592         }
593         return true;
594      }
595
596      return false;
597   }
598
599   /**
600    * Disconnect a connected console session
601    *
602    * @return boolean
603    */
604   public function disconnect()
605   {
606      if ($this->socket != null) {
607         fclose($this->socket);
608         $this->socket = null;
609         if ($this->config['debug']) {
610            echo "Connection to " . $this->config['host'] . " on port " . $this->config['port'] . " closed\n";
611         }
612         return true;
613      }
614
615      return false;
616   }
617
618   /**
619    * Login into a Bareos Director e.g. authenticate the console session
620    *
621    * @return boolean
622    */
623   private function login()
624   {
625      include 'version.php';
626
627      if(isset($this->config['console_name'])) {
628         $bashed_console_name = self::bash_spaces($this->config['console_name']);
629         $DIR_HELLO = "Hello " . $bashed_console_name . " calling version $bareos_full_version\n";
630      } else {
631         $DIR_HELLO = "Hello *UserAgent* calling\n";
632      }
633
634      self::send($DIR_HELLO);
635      $recv = self::receive();
636
637      self::cram_md5_response($recv, $this->config['password']);
638      $recv = self::receive();
639
640      if(strncasecmp($recv, self::DIR_AUTH_FAILED, strlen(self::DIR_AUTH_FAILED)) == 0) {
641         return false;
642         //throw new \Exception("Failed to authenticate with Director\n");
643      } elseif(strncasecmp($recv, self::DIR_OK_AUTH, strlen(self::DIR_OK_AUTH)) == 0) {
644         return self::cram_md5_challenge($this->config['password']);
645      } else {
646         return false;
647         //throw new \Exception("Unknown response to authentication by Director $recv\n");
648      }
649
650   }
651
652   /**
653    * Verify the CN of the certificate against a list of allowed CN names.
654    *
655    * @return boolean
656    */
657   private function tls_postconnect_verify_cn()
658   {
659      if ($this->socket) {
660        return false;
661      }
662
663      $options = stream_context_get_options($this->socket);
664
665      if (isset($options['ssl']) && isset($options['ssl']['peer_certificate'])) {
666         $cert_data = openssl_x509_parse($options["ssl"]["peer_certificate"]);
667
668         if ($this->config['debug']) {
669            print_r($cert_data);
670         }
671
672         if (isset($cert_data['subject']['CN'])) {
673            $common_names = $cert_data['subject']['CN'];
674            if ($this->config['debug']) {
675               echo("CommonNames: " . $common_names . "\n");
676            }
677         }
678
679         if (isset($common_names)) {
680            $checks = explode(',', $common_names);
681
682            foreach($checks as $check) {
683               $allowed_cns = explode(',', $this->config['allowed_cns']);
684               foreach($allowed_cns as $allowed_cn) {
685                  if (strcasecmp($check, $allowed_cn) == 0) {
686                     return true;
687                  }
688               }
689            }
690         }
691      }
692
693      return false;
694   }
695
696   /**
697    * Verify TLS names
698    *
699    * @param $names
700    * @return boolean
701    */
702   private function verify_tls_name($names)
703   {
704      $hostname = $this->config['host'];
705      $checks = explode(',', $names);
706
707      $tmp = explode('.', $hostname);
708      $rev_hostname = array_reverse($tmp);
709      $ok = false;
710
711      foreach($checks as $check) {
712         $tmp = explode(':', $check);
713
714         /*
715          * Candidates must start with DNS:
716          */
717         if ($tmp[0] != 'DNS') {
718            continue;
719         }
720
721         /*
722          * and have something afterwards
723          */
724         if (!isset($tmp[1])) {
725            continue;
726         }
727
728         $tmp = explode('.', $tmp[1]);
729
730         /*
731          * "*.com" is not a valid match
732          */
733         if (count($tmp) < 3) {
734            continue;
735         }
736
737         $cand = array_reverse($tmp);
738         $ok = true;
739
740         foreach($cand as $i => $item) {
741            if (!isset($rev_hostname[$i])) {
742               $ok = false;
743               break;
744            }
745
746            if ($rev_hostname[$i] == $item) {
747               continue;
748            }
749
750            if ($item == '*') {
751               break;
752            }
753         }
754
755         if ($ok) {
756            break;
757         }
758      }
759
760      return $ok;
761   }
762
763   /**
764    * Verify the subjectAltName or CN of the certificate against the hostname we are connecting to.
765    *
766    * @return boolean
767    */
768   private function tls_postconnect_verify_host()
769   {
770      if (!$this->socket) {
771        return false;
772      }
773
774      $options = stream_context_get_options($this->socket);
775
776      if (isset($options['ssl']) && isset($options['ssl']['peer_certificate'])) {
777         $cert_data = openssl_x509_parse($options["ssl"]["peer_certificate"]);
778
779         if ($this->config['debug']) {
780            print_r($cert_data);
781         }
782
783         /*
784          * Check subjectAltName extensions first.
785          */
786         if (isset($cert_data['extensions'])) {
787            if (isset($cert_data['extensions']['subjectAltName'])) {
788               $alt_names = $cert_data['extensions']['subjectAltName'];
789               if ($this->config['debug']) {
790                  echo("AltNames: " . $alt_names . "\n");
791               }
792
793               if (self::verify_tls_name($alt_names)) {
794                  return true;
795               }
796            }
797         }
798
799         /*
800          * Try verifying against the subject name.
801          */
802         if (isset($cert_data['subject']['CN'])) {
803            $common_names = "DNS:" . $cert_data['subject']['CN'];
804            if ($this->config['debug']) {
805               echo("CommonNames: " . $common_names . "\n");
806            }
807
808            if (self::verify_tls_name($common_names)) {
809               return true;
810            }
811         }
812      }
813
814      return false;
815   }
816
817   /**
818    * Perform a CRAM MD5 response
819    *
820    * @param $recv
821    * @param $password
822    * @return boolean
823    */
824   private function cram_md5_response($recv, $password)
825   {
826      list($chal, $ssl) = sscanf($recv, "auth cram-md5 %s ssl=%d");
827
828      switch($ssl) {
829         case self::BNET_TLS_OK:
830            $this->config['server_can_do_tls'] = true;
831            break;
832         case self::BNET_TLS_REQUIRED:
833            $this->config['server_requires_tls'] = true;
834            break;
835         default:
836            $this->config['server_can_do_tls'] = false;
837            $this->config['server_requires_tls'] = false;
838            break;
839      }
840
841      $m = hash_hmac('md5', $chal, md5($password), true);
842      $msg = rtrim(base64_encode($m), "=");
843
844      self::send($msg);
845
846      return true;
847   }
848
849   /**
850    * Perform a CRAM MD5 challenge
851    *
852    * @param $password
853    * @return boolean
854    */
855   private function cram_md5_challenge($password)
856   {
857      $rand = rand(1000000000, 9999999999);
858      $time = time();
859      $clientname = "php-bsock";
860      $client = "<" . $rand . "." . $time . "@" . $clientname . ">";
861
862      if($this->config['client_requires_tls']) {
863         $DIR_AUTH = sprintf("auth cram-md5 %s ssl=%d\n", $client, self::BNET_TLS_REQUIRED);
864      } elseif($this->config['client_can_do_tls']) {
865         $DIR_AUTH = sprintf("auth cram-md5 %s ssl=%d\n", $client, self::BNET_TLS_OK);
866      } else {
867         $DIR_AUTH = sprintf("auth cram-md5 %s ssl=%d\n", $client, self::BNET_TLS_NONE);
868      }
869
870      if(self::send($DIR_AUTH) == true) {
871         $recv = self::receive();
872         $m = hash_hmac('md5', $client, md5($password), true);
873
874         $b64 = new BareosBase64();
875         $msg = rtrim( $b64->encode($m, false), "=" );
876
877         if (self::send(self::DIR_OK_AUTH) == true && strcmp(trim($recv), trim($msg)) == 0) {
878            return true;
879         } else {
880            return false;
881         }
882      } else {
883         return false;
884      }
885
886   }
887
888   /**
889    * Send a single command
890    *
891    * @param $cmd
892    * @param $api
893    * @return string
894    */
895   public function send_command($cmd, $api=0, $jobid=null)
896   {
897      $result = "";
898      $debug = "";
899
900      switch($api) {
901         case 2:
902            // Enable api 2 with compact mode enabled
903            self::send(".api 2 compact=yes");
904            try {
905               $debug = self::receive_message();
906               if(!preg_match('/result/', $debug)) {
907                  throw new \Exception("Error: API 2 not available on director.
908                  Please upgrade to version 15.2.2 or greater and/or compile with jansson support.");
909               }
910            }
911            catch(\Exception $e) {
912               echo $e->getMessage();
913               exit;
914            }
915            break;
916         case 1:
917            self::send(".api 1");
918            $debug = self::receive_message();
919            break;
920         default:
921            self::send(".api 0");
922            $debug = self::receive_message();
923            break;
924      }
925
926      if (isset($this->config['catalog'])) {
927         if(self::send("use catalog=" . $this->config['catalog'])) {
928            $debug = self::receive_message();
929         }
930      }
931
932      if($jobid != null) {
933         if(self::send(".bvfs_update jobid=$jobid")) {
934            $debug = self::receive_message();
935         }
936      }
937
938      if(self::send($cmd)) {
939         $result = self::receive_message();
940      }
941
942      return $result;
943   }
944
945   /**
946    *
947    *
948    * @param $type
949    * @param $jobid
950    * @param $client
951    * @param $restoreclient
952    * @param $restorejob
953    * @param $where
954    * @param $fileid
955    * @param $dirid
956    * @param $jobids
957    *
958    * @return string
959    */
960   public function restore($type=null, $jobid=null, $client=null, $restoreclient=null, $restorejob=null, $where=null, $fileid=null, $dirid=null, $jobids=null, $replace=null)
961   {
962      $result = "";
963      $debug = "";
964      $rnd = rand(1000,1000000);
965
966      if(self::send(".api 0")) {
967         $debug = self::receive_message();
968      }
969
970      if(self::send(".bvfs_update jobid=$jobids")) {
971         $debug = self::receive_message();
972      }
973
974      if(self::send(".bvfs_restore jobid=$jobids fileid=$fileid dirid=$dirid path=b2000$rnd")) {
975         $debug = self::receive_message();
976      }
977
978      if(self::send('restore file=?b2000'.$rnd.' client="'.$client.'" restoreclient="'.$restoreclient.'" restorejob="'.$restorejob.'" where="'.$where.'" replace="'.$replace.'" yes')) {
979         $result = self::receive_message();
980      }
981
982      if(self::send(".bvfs_cleanup path=b2000$rnd")) {
983         $debug = self::receive_message();
984      }
985
986      return $result;
987   }
988
989}
990?>
991