#!/usr/bin/perl -w
#
# ================================================== #
#                      Slapo-CGP                     #
#                                                    #
#                                                    #
# ---------- (c) AO StalkerSoft 2015-2019 ---------- #
# ================================================== #
#
# slapo-cgp
# The script represents an overlay interface for CommuniGate Pro
# integration with OpenLDAP. It uses the slapo-sock OpenLDAP module.
#
# For technical support please contact <support@communigate.com>.

######## Please redefine these values
my $CGServerName = ["172.18.18.161","172.18.18.162"];
my $CGServerPort = 106;
my $Login = "abc\@postmaster.local";
my $Password = "qwe1234";
my $UseSSL = 0;

my $fields = {
  cn => "RealName",
# sn_givenName_initials => "RealName",
  userPassword => "Password",
  givenName => "givenName",
  initials => "initials",
  city => "l",
  countryUser => "c",
  storageLimit => "MaxAccountSize",
  sn => "sn",
};

my $SlapdSock = "/var/tmp/slapd.sock";
my $LogFile = "/var/log/slapo-cgp.log";
my $PingPeriod = 60;
######## End of the customizeable area

my $CLI_CODE_ACCOUNT_EXISTS = 520;

use strict;
use warnings;
use CGP::CLI;
use IO::Socket::UNIX;
use IO::Handle;
use MIME::Base64;
use Data::Dumper;
use utf8;

sub daemonize {
  use POSIX;
  POSIX::setsid or die "setsid: $!";
  my $pid = fork();
  if($pid < 0) {
    die "fork: $!";
  } elsif($pid) {
    exit 0;
  }
  chdir "/";
  umask 0;
  foreach (0 .. (POSIX::sysconf (&POSIX::_SC_OPEN_MAX) || 1024)) {
    POSIX::close $_;
  }
  open(STDIN, "</dev/null");
  open(STDOUT, ">/dev/null");
  open(STDERR, ">&STDOUT");
}

sub parseLDAPRequest {
  my $conn = shift;
  unless($conn->connected) {
    return undef;
  }
  my $request = {};
  $request->{type} = $conn->getline();
  chomp($request->{type});
  $request->{attributes} = [];

  my @lines=();
  while(my $line = $conn->getline()) {
    chomp($line);
    last if($line eq "");
    if($line=~/^\s+(.*)/ && scalar(@lines)>0) {
      $lines[scalar(@lines) -1 ].=$1;
    } else {
      push(@lines,$line);
    }
  }

  while(my $line = shift(@lines) ) {
    last if($line eq "");

    if($line =~ /^([^:]+):\s*(.*)$/) {
      if($1 eq "add" || $1 eq "replace" || $1 eq "delete") {
        my $changeType = $1;
        unless($request->{$changeType}) {
          $request->{$changeType} = {};
        }
        if($changeType eq "delete") {
          $request->{$changeType}->{$2} = "#NULL#";
          next;
        }
        while($line = shift(@lines) ) {
          last if($line eq "-" || $line eq "");
          if($line =~ /^([^:]+):\s*(.*)$/) {
            if($2 eq "none") {
              $request->{$changeType}->{$1} = "#NULL#";
            } else {
              if($1 eq "userPassword") {
                $request->{$changeType}->{$1} = decode_base64($2);
              } else {
                my ($key,$value) = ($1,$2);
                unless($value =~ m/^\:\ (.*)$/) {
                  $request->{$changeType}->{$key} = $value;
                } else {
                  $request->{$changeType}->{$key} = decode_base64($value);
                }
              }
            }
          }
        }
      } else {
        if($2 eq "none") {
          push(@{$request->{attributes}}, { $1 => "#NULL#" });
        } else {
          if($1 eq "userPassword") {
            push(@{$request->{attributes}}, { $1 => decode_base64($2) });
          } else {
            my ($key,$value) = ($1,$2);
            unless($value =~ m/^\:\ (.*)$/) {
              push(@{$request->{attributes}}, { $key => $value });
            } else {
              push(@{$request->{attributes}}, { $key => decode_base64($1) });
            }
          }
        }
      }
    }
  }
  return $request;
}

sub dnToAccountName {
  my $dn = shift;
  return undef unless($dn);
  my @parts = split(/,/, $dn);
  my $userPart;
  my $domainPart = "";
  foreach my $part (@parts) {
    if($part =~ m/^uid=(..*)/) {
      $userPart = $1;
    } elsif($part =~ m/^dc=(..*)/) {
      $domainPart .= $1 . ".";
    }
  }
  chop($domainPart);
  return $userPart . "@" . $domainPart;
}

sub getLoggingTime {
  my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
  my $nice_timestamp = sprintf("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900,$mon+1,$mday,$hour,$min,$sec);
  return $nice_timestamp;
}

sub flush {
  my $h = select($_[0]); my $af=$|; $|=1; $|=$af; select($h);
}

my $logFh;
sub writeLog {
  my $str = shift;
  print $logFh getLoggingTime() . ': ' . $str . "\n";
  flush($logFh);
}

sub reconnect {
  my $newCli;
  my $newCurrentServer;
  while(!$newCli) {
    if(ref($CGServerName) eq "ARRAY") {
      foreach my $name (@{$CGServerName}) {
        writeLog("reconnecting to " . $name);
        $newCli = new CGP::CLI({
          PeerAddr => $name,
          PeerPort => $CGServerPort,
          login    => $Login,
          password => $Password,
          SecureLogin => 1,
          SSLTransport => $UseSSL,
        });
        if($newCli) {
          $newCurrentServer = $name;
          last;
        }
        sleep(10);
      }
    } elsif(!defined(ref($CGServerName))) {
      writeLog("reconnecting to " . $CGServerName);
      $newCli = new CGP::CLI({
        PeerAddr => $CGServerName,
        PeerPort => $CGServerPort,
        login    => $Login,
        password => $Password,
        SecureLogin => 1,
        SSLTransport => $UseSSL,
      });
      $newCurrentServer = $CGServerName;
      unless($newCli) {
        sleep(10);
      }
    }
  }
  writeLog("PWD connection established: $Login\@$newCurrentServer:$CGServerPort");
  return $newCli;
}

daemonize();

open($logFh, '>>', $LogFile) || die "Could not open log file $LogFile: $!";

writeLog("slapo-cgp started");

# create slapd socket
unlink($SlapdSock);
my $SOCK = IO::Socket::UNIX->new(
  Type => SOCK_STREAM(),
  Local => $SlapdSock,
  Listen => 1,
  Blocking => 0,
);
unless($SOCK) {
  writeLog("can't create socket " . $SlapdSock);
  writeLog("slapo-cgp stopped");
  close($logFh);
  exit 1;
}
IO::Handle::blocking($SOCK, 0);
writeLog("socket created: $SlapdSock");

# connect to CGP
my $cli;
my $currentServer;
if(ref($CGServerName) eq "ARRAY") {
  foreach my $name (@{$CGServerName}) {
    $cli = new CGP::CLI({
      PeerAddr => $name,
      PeerPort => $CGServerPort,
      login    => $Login,
      password => $Password,
      SecureLogin => 1,
      SSLTransport => $UseSSL,
    });
    if($cli) {
      $currentServer = $name;
      last;
    }
  }
} elsif(!defined(ref($CGServerName))) {
  $cli = new CGP::CLI({
    PeerAddr => $CGServerName,
    PeerPort => $CGServerPort,
    login    => $Login,
    password => $Password,
    SecureLogin => 1,
    SSLTransport => $UseSSL,
  });
  $currentServer = $CGServerName;
} else {
  writeLog("bad CGServerName parameter:\n" . Dumper($CGServerName));
  close($SOCK);
  writeLog("socket closed");
  writeLog("slapo-cgp stopped");
  close($logFh);
  exit 1;
}
unless($cli) {
  writeLog("can't login to CGPro: " . $CGP::ERR_STRING);
  close($SOCK);
  writeLog("socket closed");
  writeLog("slapo-cgp stopped");
  close($logFh);
  exit 1;
}
writeLog("PWD connection established: $Login\@$currentServer:$CGServerPort");

my $timer = time();

my $continue = 1;
$SIG{TERM} = sub {
  $continue = 0;
};

while($continue) {
  while(my $conn = $SOCK->accept()) {
    my $request = parseLDAPRequest($conn);
    unless($request) {
      writeLog("socket connection error: could not read from socket");
      last;
    }
    writeLog("processing request:\n" . Dumper($request));
    unless($request->{attributes}) {
      writeLog("skip: attributes are missing");
      close($conn);
      last;
    }
    my $accountName;
    foreach my $attribute (@{$request->{attributes}}) {
      while(my ($name, $value) = each(%$attribute)) {
        if($name eq "dn") {
          $accountName = dnToAccountName($value);
        }
      }
      last if($accountName);
    }
    unless($accountName) {
      writeLog("skip: could not convert dn -> accountName");
      close($conn);
      last;
    }
    if($request->{type} eq "ADD") {
      my $userData = {};
      my $sn;
      my $givenName;
      my $initials;
      foreach my $attribute (@{$request->{attributes}}) {
        while(my ($name, $value) = each(%$attribute)) {
          if($name eq 'sn') {
            $sn = $value;
          } elsif($name eq 'givenName') {
            $givenName = $value;
          } elsif($name eq 'initials') {
            $initials = $value;
          }
          if(exists($fields->{$name})) {
            if($value eq "#NULL#") {
              $userData->{$fields->{$name}} = undef;
            } else {
              $userData->{$fields->{$name}} = $value;
            }
          }
        }
      }
      if(exists($fields->{sn_givenName_initials}) && !$userData->{RealName}) {
        $userData->{RealName} = '';
        if($sn) {
          $userData->{RealName} .= $sn;
        }
        if($givenName) {
          if($userData->{RealName}) {
            $userData->{RealName} .= " ";
          }
          $userData->{RealName} .= $givenName;
        }
        if($initials) {
          if($userData->{RealName}) {
            $userData->{RealName} .= " ";
          }
          $userData->{RealName} .= $initials;
        }
      }
      writeLog("execute create account CLI $accountName, settings:\n" . Dumper($userData));
      my $result = $cli->CreateAccount(
        accountName => $accountName,
        settings => $userData,
      );
      unless($result) {
        if($cli->getErrCode == $CLI_CODE_ACCOUNT_EXISTS) {
          writeLog("got account exists response, execute update account settings CLI $accountName, settings:\n" . Dumper($userData));
          $result = $cli->UpdateAccountSettings($accountName, $userData);
          unless($result) {
            writeLog("CGPro error: " . $cli->getErrMessage);
          }
        } else {
          writeLog("CGPro error: " . $cli->getErrMessage);
        }
      }
      $timer = time();
    } elsif($request->{type} eq "MODIFY") {
      my $userData = {};
      foreach my $changeType (qw(add replace delete)) {
        if($request->{$changeType}) {
          while(my ($LDAPField, $CGField) = each(%$fields)) {
            if(exists($request->{$changeType}->{$LDAPField})) {
              if($request->{$changeType}->{$LDAPField} eq "#NULL#") {
                $userData->{$CGField} = undef;
              } else {
                $userData->{$CGField} = $request->{$changeType}->{$LDAPField};
              }
            }
          }
        }
      }
      if(scalar(keys %$userData)) {
        writeLog("execute update account settings CLI $accountName, settings:\n" . Dumper($userData));
        my $result = $cli->UpdateAccountSettings($accountName, $userData);
        unless($result) {
          writeLog("CGPro error: " . $cli->getErrMessage);
        }
        $timer = time();
      } else {
        writeLog("nothing to modify");
      }
    } elsif($request->{type} eq "DELETE") {
      writeLog("execute delete account CLI $accountName");
      my $result = $cli->DeleteAccount($accountName);
      unless($result) {
        writeLog("CGPro error: " . $cli->getErrMessage);
      }
      $timer = time();
    } else {
      writeLog("skip: unknown request type");
      close($conn);
      last;
    }
    print $conn "CONTINUE\n\n";
  }
  if(time() - $timer >= $PingPeriod) {
    my $result = $cli->Noop();
    unless($result) {
      writeLog("PWD connection error: " . $cli->getErrMessage);
      $cli = reconnect();
    }
    $timer = time();
  }
}

$cli->Logout;
writeLog("PWD connection closed");

close($SOCK);
writeLog("socket closed");

writeLog("slapo-cgp stopped");
close($logFh);
