#!/usr/bin/perl
# 
# ***** BEGIN LICENSE BLOCK *****
# Zimbra Collaboration Suite Server
# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2013, 2014, 2015, 2016 Synacor, Inc.
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software Foundation,
# version 2 of the License.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along with this program.
# If not, see <https://www.gnu.org/licenses/>.
# ***** END LICENSE BLOCK *****
# 

use strict;
#
# We allow only a well-known set of commands to be executed with the
# Zimbra key.  Try to use as few regular expression checked commands
# as possible, and when you do create them be conservative with what
# is allowed - specially beware of any shell special characters.
#
my %SIMPLE_COMMANDS = (
   "start mailbox"    => "/opt/zimbra/bin/zmstorectl start",
   "start ldap"       => "/opt/zimbra/bin/ldap start",
   "start mta"        => "/opt/zimbra/bin/zmmtactl start",
   "start antispam"   => "/opt/zimbra/bin/zmantispamctl start",
   "start antivirus"  => "/opt/zimbra/bin/zmantivirusctl start",
   "start snmp"       => "/opt/zimbra/bin/zmswatchctl start",
   "start spell"      => "/opt/zimbra/bin/zmspellctl start",
   "stop mailbox"     => "/opt/zimbra/bin/zmstorectl stop",
   "stop ldap"        => "/opt/zimbra/bin/ldap stop",
   "stop mta"         => "/opt/zimbra/bin/zmmtactl stop",
   "stop antispam"    => "/opt/zimbra/bin/zmantispamctl stop",
   "stop antivirus"   => "/opt/zimbra/bin/zmantivirusctl stop",
   "stop snmp"        => "/opt/zimbra/bin/zmswatchctl stop",
   "stop spell"       => "/opt/zimbra/bin/zmspellctl stop",
   "status"           => "/opt/zimbra/bin/zmcontrol status",
   "startup"          => "/opt/zimbra/bin/zmcontrol startup",
   "shutdown"         => "/opt/zimbra/bin/zmcontrol shutdown",
   "msgtrace"         => "/opt/zimbra/bin/zmmsgtrace",
   "flushqueue"       => "/opt/zimbra/common/sbin/postqueue -f",
   "showqueue"        => "/opt/zimbra/common/sbin/postqueue -p",
   "zmserverips"      => "/opt/zimbra/libexec/zmserverips",
   "zmupdateauthkeys" => "/opt/zimbra/bin/zmupdateauthkeys",
   "slapcat"          => "/opt/zimbra/common/sbin/slapcat -F /opt/zimbra/data/ldap/config -b ''",
   "zmqstat all"      => "sudo /opt/zimbra/libexec/zmqstat",
   "zmqstat incoming" => "sudo /opt/zimbra/libexec/zmqstat incoming",
   "zmqstat hold"     => "sudo /opt/zimbra/libexec/zmqstat hold",
   "zmqstat active"   => "sudo /opt/zimbra/libexec/zmqstat active",
   "zmqstat deferred" => "sudo /opt/zimbra/libexec/zmqstat deferred",
   "zmqstat corrupt"  => "sudo /opt/zimbra/libexec/zmqstat corrupt",
   "zmcollectconfigfiles" => "tar cv /opt/zimbra/conf | gzip -cf",
   "zmcollectldapzimbra" => "/opt/zimbra/common/sbin/slapcat -F /opt/zimbra/data/ldap/config -b '' -s cn=zimbra | gzip -cf",
   "downloadcsr"      => "cat /opt/zimbra/ssl/zimbra/commercial/commercial.csr"
);


# Regexes for Postfix Queue IDs

# Short Format character: ASCII uppercase A-F range plus ASCII digits
my $SF_QID_CHAR = qr{[A-F0-9]};

# Long Format time portion character:  ASCII digits and ASCII uppercase/lowercase consonants
my $LF_QID_TIME_CHAR  = qr{[0-9BCDFGHJKLMNPQRSTVWXYZ]}i;

# Long Format inode number portion character: ASCII digits and ASCII uppercase/lowercase consonants minus "z"
my $LF_QID_INODE_CHAR = qr{[0-9BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxy]};

my $REGEX_POSTFIX_QID = qr{(?:${SF_QID_CHAR}{6,}+|${LF_QID_TIME_CHAR}{10,}z${LF_QID_INODE_CHAR}++)};


my %REGEX_CHECKED_COMMANDS = (
  "zmqaction" => {
    regex => qr{(hold|release|requeue|delete)\s+(incoming|deferred|corrupt|active|maildrop|hold)\s+(?:(?:${REGEX_POSTFIX_QID},)*+${REGEX_POSTFIX_QID}|ALL)},
    program => "/opt/zimbra/libexec/zmqaction"},
  "zmbackupldap" => {
    regex => '[a-zA-Z0-9\\/\\.\\-_:@ ]*',
    # typcial backup destination path:
    # alphanumeric, slash, dot, dash, underscore, colon, at, space
    program => "/opt/zimbra/libexec/zmbackupldap"},
  "zmcertmgr" => {
    regex => '(viewdeployedcrt|viewstagedcrt|createcsr|createcrt|getcrt|deploycrt|viewcsr|verifycrt|verifycrtkey|verifycrtchain)\s?[a-zA-Z0-9\\/\\.\\-\\\_:@\\,\\=\\\'\\"\\* ]*',
    program => "/opt/zimbra/bin/zmcertmgr"},
  "clusvcadm" => {
    regex => '[a-zA-Z0-9\\/\\.\\-_:@ ]*',
    # alphanumeric, slash, dot, dash, underscore, colon, at, space
    program => "sudo /usr/sbin/clusvcadm"},
  "zmclustat" => {
    regex => '[a-zA-Z0-9\\/\\.\\-_:@ ]*',
    # alphanumeric, slash, dot, dash, underscore, colon, at, space
    program => "sudo /opt/zimbra-cluster/bin/zmclustat"},
   "zmloggerhostmap"  => {
     regex => '[a-zA-Z0-9\\/\\.\\-_:@ ]*',
     program=>"/opt/zimbra/bin/zmloggerhostmap",},
   "zmschedulebackup" => {
     regex => "[a-zA-Z0-9\\/\\.\\-\\,\\'\\*_:@ ]*",
     program => "/opt/zimbra/bin/zmschedulebackup"},
   "rsync" => {
     regex => '[\'"a-zA-Z0-9\\/\\.\\-_:@= ]*',
     program => "rsync"},
   "zmschedulesmpolicy" => {
     regex => '[\'"e-g0-9\\-: ]*',
     program => "/opt/zimbra/bin/zmschedulesmpolicy"},
);

my %allhosts = ();
my $gothosts = 0;

sub trim($) {
  my $val = shift;
  $val =~ s/[\r\n]*$//;  # Windows-safe
  return $val;
}

my $thishost = trim(qx(/opt/zimbra/bin/zmlocalconfig -m nokey zimbra_server_hostname));
my $enable_logging = uc(trim(qx(/opt/zimbra/bin/zmlocalconfig -m nokey zimbra_zmrcd_logging_enabled 2> /dev/null)));
$enable_logging = "FALSE" unless $enable_logging;


sub logMsg($) {
  my $msg = shift;
  print STDOUT "$msg\n";
  print LOG "$msg\n" if ($enable_logging eq "TRUE");
}

sub logError($) {
  my $msg = shift;
  print STDERR "ERROR: $msg\n";
  print LOG "$msg\n" if ($enable_logging eq "TRUE");
  #logMsg("ERROR: $msg");
}

sub runRemoteCommand {
  my $host = shift;
  my $command = shift;
  my $args = shift;

  logMsg("Remote: HOST:$host $command $args");
}

sub runCommand {
  my $host = shift;
  my $command = shift;
  my $args = shift;

  #logMsg("runCommand: $host $command $args");
  if (lc($host) ne lc($thishost)) {
    runRemoteCommand($host, $command, $args);
    return;
  }

  my $cmdstr;
  my $smplcmd;
  if (defined($args) && $args ne "") {  
    $smplcmd = $command . " " . $args;
  } else {
    $smplcmd = $command;
  }
  if (defined($SIMPLE_COMMANDS{$smplcmd})) {
    $cmdstr = $SIMPLE_COMMANDS{$smplcmd};
    #logMsg("SIMPLE_COMMAND: $cmdstr");
  } elsif (defined($REGEX_CHECKED_COMMANDS{$command})) {
    my %spec = %{$REGEX_CHECKED_COMMANDS{$command}};
    my $regex = $spec{regex};
    my $program = $spec{program};
    if (!defined($regex)) {
      logError("internal error (regex undefined)");
      exit 1;
    }
    if (!defined($program)) {
      logError("internal error (program undefined)");
      exit 1;
    }
    if ($args !~ /^$regex$/) {
      logError("args '$args' not allowed for command '$command'");
      exit 1;
    }
    $cmdstr = $program . " " . $args;
  } else {
    #logMsg("$SIMPLE_COMMANDS{$smplcmd}");
    logError("Unknown command: \"$command\"");
    exit 1;
  }
  if (open(COMMAND, "$cmdstr |")) {
    #logMsg("Running cmd: $cmdstr");
    if (($command ne "zmqstat") && ($command ne "zmcollectconfigfiles") && ($command ne "zmcollectldapzimbra") &&
        ($command ne "clusvcadm") && ($command ne "zmclustat")) {
      logMsg("STARTCMD: $host $cmdstr");
    }

    while (<COMMAND>) {
      chomp;
      logMsg($_);
    }
    close COMMAND;

    if (($command ne "zmqstat") && ($command ne "zmcollectconfigfiles") && ($command ne "zmcollectldapzimbra") &&
        ($command ne "clusvcadm") && ($command ne "zmclustat")) {
      logMsg("ENDCMD: $host $cmdstr");
    }

    # Stop if command exited with error.
    my $status = $? >> 8;
    if ($status != 0) {
        exit $status;
    }
  } else {
    logError("Can't run $cmdstr: $!");
    exit 1;
  }
}

sub getHostsByService {
  my $service = shift;

  my @hosts = ();

  if (!$gothosts) {
    open CMD, "/opt/zimbra/bin/zmprov -l gas |" or return undef;
    my @hl = <CMD>;
    close CMD;
    foreach my $h (@hl) {
      $h = trim($h);
      alarm(120);
      open CMD, "/opt/zimbra/bin/zmprov -l gs $h | grep zimbraServiceEnabled | sed -e 's/zimbraServiceEnabled: //'|" or return undef;
      my @sl = <CMD>;
      close CMD;
      foreach my $s (@sl) {
        $s = trim($s);
        $allhosts{$h}{$s} = $s;
      }
      alarm(0);
    }
    $gothosts = 1;
  }

  foreach my $h (keys %allhosts) {
    foreach my $s (keys %{ $allhosts{$h} }) {
      if ($s eq $service) {
        push @hosts, $h;
      }
    }
  }
  return \@hosts;
}

sub getHostList {
  my $hstring = shift;

  # Host format is either 
  #   HOST:h1[,HOST:h2...] and/or
  #   SERVICE:s1[SERVICE:s2,...]
  # The script will de-dup hosts

  my %hosts = ();

  my @hspecs = split (',', $hstring);
  foreach my $spec (@hspecs) {
    my ($type, $item) = split (':', $spec);
    if ($type eq "HOST") {
      if ($item eq "ALL") {
        getHostsByService();
        my @h = sort keys %allhosts;
        return \@h;
      }
      $hosts{$item} = $item;
    } elsif ($type eq "SERVICE") {
      if ($item eq "ALL") {
        getHostsByService();
        my @h = sort keys %allhosts;
        return \@h;
      }
      my $hl = getHostsByService($item);
      foreach (@$hl) {
        $hosts{$_} = $_;
      }
    } else {
      return undef;
    }
  }
  my @h = sort keys %hosts;
  return \@h;
}

sub isRsyncCmd {
  my $cmd = shift;
  if (defined($cmd) && $cmd ne '') {
    my @parts = split(/\s+/, $cmd);
    my $prog = $parts[0];
    if ($prog =~ /rsync$/) {
      if (($prog ne 'rsync') && ($prog ne '/opt/zimbra/common/bin/rsync')) {
        logError("command '$prog' not allowed");
        exit 1;
      }
      my $regex = $REGEX_CHECKED_COMMANDS{'rsync'}->{'regex'};
      if ($cmd !~ /^$regex$/) {
        logError("invalid arguments in command [$cmd]");
        exit 1;
      }
      return 1;
    }
  }
  return 0;
}

sub doHelp {
  foreach my $cm (sort keys %SIMPLE_COMMANDS) {
    print $cm, " -> ", $SIMPLE_COMMANDS{$cm}, "\n";
  }
  foreach my $cm (sort keys %REGEX_CHECKED_COMMANDS) {
    my %cd = %{$REGEX_CHECKED_COMMANDS{$cm}};
    print $cm, " ", $cd{regex}, " -> ", $cd{program}, " <arg>\n";
  }
}

sub handleALRM {
  logMsg("ENDCMD: Timeout reached!");
  eval {
    close CMD;
  };
}

$| = 1;

$SIG{ALRM} = \&handleALRM;
open(LOG, ">>/opt/zimbra/log/zmrcd.log")
  if ($enable_logging eq "TRUE");

# special case for rsync over ssh from a remote host
my $originalCmd = $ENV{'SSH_ORIGINAL_COMMAND'};
if (isRsyncCmd($originalCmd)) {
  print LOG "exec'ing: $originalCmd\n" if ($enable_logging eq "TRUE");
  exec($originalCmd);
}

while (<>) {
  trim($_);
  my ($host, $command, $args) = split (' ', $_, 3);

  if ($host eq "?") {
    doHelp();
    next;
  }

  my $hostlist = getHostList ($host);

  if (!defined ($hostlist)) {
    logError("Invalid hostlist");
    exit 1;
  }

  foreach my $h (@$hostlist) {
    runCommand ($h, $command, $args);
  }
  close(LOG) if ($enable_logging eq "TRUE");
  exit 0;

}
