#!/usr/bin/env python

from __future__ import print_function

#
# Rev: 2026011203 - II: use a more efficient way to execute FOREACH DOMAIN
# Rev: 2026011202 - II: fix for processing LIST STORAGE output
# Rev: 2026011201 - II: add support for pipe character (to be encoded as %7C%)
# Rev: 2024100901 - II: change regex string to raw
# Rev: 2024100201 - II: enable logging to stdout only after successful authentication
# Rev: 2024092601 - II: add support for updating WM and WA filters
# Rev: 2024092501 - II: add support for reading from stdin (use - for cmds)
# Rev: 2024030701 - II: add support for removing items from FOREACH command (via REMOVE-ITEM)
# Rev: 2021112501 - II: add support for changing FOREACH default action (back)
# Rev: 2020061801 - II: add support for python 3
# Rev: 2020041601 - II: add support for environment variable CLIPASS
# Rev: 2020022503 - II: add support for SHOW and SHOW ATTR
# Rev: 2020022502 - II: add information about FOREACH object name (using 'info')
# Rev: 2020022501 - II: fix for FOREACH command (add a back command)
# Rev: 2019052903 - II: add support FOREACH virtual command
# Rev: 2019052902 - II: change CLIRESPONSE values (1=True=default|0=False|2=minimal)
# Rev: 2019052901 - II: add support for SSL
# Rev: 2019052701 - II: do not depend on cli2.py anymore
# Rev: 2018103103 - II: change the output when CLI command fails
# Rev: 2018103102 - II: add support for environment variable CLIRESPONSE (True=default|False)
# Rev: 2018103101 - II: add support for environment variables CLIHOST and CLIPORT
# Rev: 2017090901 - II: initial release
#

"""
Run CLI commands separated by pipe
"""
"""
Copyright (c) since 2006, Axigen Messaging. All rights reserved.
For feedback and/or bugs in this script, please send an e-mail to:
  "AXIGEN Team" <team@axigen.com>
"""

### defaults ###
CLIHOST='127.0.0.1'
CLIPORT=7000
CLIUSER='admin'
CLIPASS='admin-password'
CLICONN='RAW'
################


############################# from cli2.py #############################
import socket, sys
from sys import stdout as STDOUT
from sys import stdin  as STDIN
import re
import os
import ssl

CRLF='\r\n'

def getEntries(sText):
  sArray = sText.split(CRLF)
  if len(sArray) >= 5:
    if sArray[1]:
      if sArray[1][-1] == ":":
        if sArray[4]:
          if sArray[4][0] == "-":
            # Normal list output (like LIST ACCOUNTS)
            return sArray[5:-3]
        # Output like LIST ALIASES
        return sArray[2:-3]
    else:
      # Output like LIST STORAGES
      return sArray[2:-3]
  if sArray[0]:
    if sArray[0][0] == " ":
      # No header (like LIST USERMAP CACHE)
      return sArray[0:-2]
    else:
      # Remove header (like LIST CLUSTERADDRESSES)
      return sArray[1:-2]

class CLIBase:
  """
  AXIGEN CLI base class
  This class defines methods for (almost) all AXIGEN CLI's commands.
  """
  debug=0
  context=None
  contexts=[]
  prompt=None
  V={
    'version': None,
    'fullversion': None,
    'platform': None,
    'os': None,
    'datadir': None
  }
  __c=re.compile(r'((((\+OK)|(-ERR)):?.*)|(Backing up done))\r\n\<([A-Za-z0-9\-]*#?)\> ')
  __noauth=True

  def __init__(self, host='localhost', port=7000, user=None, passwd=None, conn='raw'):
    """
    Initialize a new CLI class instance.

    The class constructor creates a socket based on the 'host' and 'port'
    parameters and, optionally, authenticates as the 'admin' user with a
    specified password by calling the auth() method
    """
    self.host=host
    self.port=port
    self.sockBase=None
    self.sock=None
    self.conn=conn
    self.user=user
    self.passwd=passwd
    self.esetStart=False
    try:
      self.sockBase=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    except socket.error:
      if self.sockBase:
        self.sockBase=None
      self.sockBase=None
    if not self.sockBase:
      raise socket.error("Connect error")

    if self.conn.upper() == 'RAW':
      self.sock = self.sockBase
    elif self.conn.upper() == 'SSL':
      self.sock = ssl.wrap_socket(self.sockBase)
    else:
      raise Exception("Connection '"+self.conn+"' type not supported.")

    self.sock.connect((host, port))
    self.__get()
    if user!=None and passwd!=None:
      self.auth(passwd,user)

  def __del__(self):
    """
    Class instance destructor.
    Closes the connection, if opened
    """
    if self.sock:
      self.sock.close()
    if self.sockBase:
      self.sockBase.close()

  def __log(self, s):
    """
    Private method for logging the output of the socket communication to stdout
    """
    if self.debug == 1:
        if not self.__noauth:
          STDOUT.write(s.replace(CRLF,"\n"))
    else:
      STDOUT.write('.')
    STDOUT.flush()


  def __get(self):
    """
    Private method used by the response() method
    """
    respl=''
    while True:
      resp=self.sock.recv(65536)
      if sys.version_info[0] > 2:
        resp=resp.decode('utf-8')
      if self.debug >= 1:
        self.__log(resp)
      respl+=resp
      l=respl.split(CRLF)
      if len(resp)>0:
        if self.__noauth:
          if l[-1][-2:]=='> ' and l[-1][0]=='<':
            break
        else:
          if self.__c.search(respl):
            break
      else:
        break
    self.prompt=l[len(l)-1]
    self.context=l[len(l)-1][1:-2]
    if len(self.context)>0:
      if self.context[len(self.context)-1]=='#':
        self.context=self.context[:-1]
      if self.context=='':
        self.contexts=[]
      else:
        self.contexts=self.context.split('-')
    return respl

  def __put(self, cmd, localdebug=None):
    """
    Private method used by the cmd() method.
    """
    cmdcrlf = cmd+CRLF
    if sys.version_info[0] < 3:
      self.sock.send(cmdcrlf)
    else:
      self.sock.send(str.encode(cmdcrlf))
    if localdebug!=None:
      if localdebug==1:
        self.__log(cmd+CRLF)
      return
    if self.debug==1:
      self.__log(cmd+CRLF)

  def cmd(self, s):
    """
    Sends a command to the CLI server.

    NOTE: The CRLF pair is added automatically to the command.
    """
    self.__put(s)

  def response(self):
    """
    Reads responses from the socket.
    If the debug=1, it outputs the response to the stdout.
    Also, it sets the current context (context and contexts variables).
    Returns a string containing the socket response.
    """
    return self.__get()

  def parse_resp(self, response):
    """
    Parses a response text (usually returned by the cmdr() or response() methods) and returns the tuple:
    code - integer with the value of 0 (successful +OK response) or 1 (error +ERR response)
    text - the exact response in addition to the +ERR or +OK text (server error reason string)
    response - contains the response, exactly the one given as argument
    """
    if not response:
      return (0, '', response)
    llines=response.split(CRLF)
    lastline=llines[len(llines)-2]
    firstspace=lastline.find(' ')
    if lastline[0]=='+':
      text=lastline[max(0,firstspace):].rstrip()
      code=1
    elif lastline[0]=='-':
      text=lastline[max(0,firstspace):].rstrip()
      code=0
    else:
      text=lastline.rstrip()
      code=0
    return (code, text, response)

  def auth(self, passwd, user='admin'):
    """
    Authenticates the CLI session. If either 'user' or 'passwd' are incorrect,
    it raises an exception
    """
    (code, text, _r)=self.parse_resp(self.cmdr('USER '+user))
    (code, text, _r)=self.parse_resp(self.cmdr(passwd))
    if not code:
      raise Exception('Authentication failed. Server response:' + text)
      return False
    self.__noauth=False
    return True

  def cmdr(self, s):
    """
    Wrapper to send a command and read its response in one call. Uses the cmd()
    and response() methods.
    """
    self.cmd(s)
    return self.response()


  def runCli(self, s, info=''):
    my_i = -1
    my_sep = '|'
    my_cmds = s.split(my_sep)
    my_loop = True
    for my_s in my_cmds:
      my_i=my_i+1
      s = my_s.strip().replace('%7C%', '|')
      if not self.esetStart:
        tstart = datetime.datetime.now()
      if ( s.upper().startswith('ESET ')  or
           s.upper() == 'UPDATE WMFILTER' or
           s.upper() == 'UPDATE WAFILTER' ) and not self.esetStart:
        self.sock.send(str.encode(s + CRLF))
        my_eset = s
        while True:
          resp = self.sock.recv(4096)
          break
        self.esetStart = True
      elif self.esetStart:
        if my_s:
          self.sock.send(str.encode(my_s + CRLF))
        else:
          self.sock.send(str.encode(CRLF + '.' + CRLF))
          while True:
            resp = self.sock.recv(4096)
            break
          self.esetStart = False
          s = my_eset
      else:
        listEntries=None
        showEntries=None
        if s.upper().startswith('FOREACH '):
          # my_loop = False
          obj = s.split()[1].upper()
          action = s.split()[-1].upper()
          if (action == obj):
            action = ''
          if obj.upper() == 'DOMAIN':
            # Switch to a more eficient way to obtain the domain list
            (code, text, resp)=self.parse_resp(self.cmdr('LIST ALLDOMAINS'))
            # Remove disabled domains
            resp = CRLF.join(line for line in resp.split(CRLF) if not line.endswith(' disabled'))
          else:
            (code, text, resp)=self.parse_resp(self.cmdr('LIST ' + obj + 'S'))
          if code:
            listEntries = getEntries(resp)
            for l in listEntries:
              my_l = l.split()[0]
              if action == 'REMOVE-ITEM':
                (code, text, resp)=self.parse_resp(self.cmdr('REMOVE ' + obj + ' ' + my_l))
                continue
              else:
                (code, text, resp)=self.parse_resp(self.cmdr('UPDATE ' + obj + ' ' + my_l))
              my_loop = False            
              self.runCli(my_sep.join(my_cmds[my_i+1:]), info = info + ':' + my_l)
              if action:
                (code, text, resp)=self.parse_resp(self.cmdr(action))
              else:
                (code, text, resp)=self.parse_resp(self.cmdr('BACK'))
        else:
          (code, text, resp)=self.parse_resp(self.cmdr(s))
          if code:
            if s.upper().startswith('LIST '):
              listEntries = getEntries(resp)
            elif s.upper().startswith('SHOW ATTR '):
              showEntries=resp.split(CRLF)[0]
            elif s.upper().startswith('SHOW'):
              showEntries='\t'.join(resp.split(CRLF)[2:-3])
      tend = datetime.datetime.now()
      try:
        context = resp.split('\n')[-1].strip()
      except:
        context = '<none>'
      if CLIBase.debug >= 1:
        print
      if not code:
        print("%-60s %-20s (%s) %-20s %s" % (info + ':' + s, 'command failure', str(tend - tstart), context, text))
        sys.exit(1)
      else:
        if not self.esetStart:
          if listEntries:
            print("%-60s %-20s (%s) %-20s %i objects" % (info + ':' + s, text, str(tend - tstart), context, len(listEntries)))
          elif showEntries:
            print("%-60s %-20s (%s) %-20s result: %s" % (info+':'+s, text, str(tend - tstart), context, showEntries))
          else:
            print("%-60s %-20s (%s) %-20s" % (info + ':' + s, text, str(tend - tstart), context))
      if not my_loop:
        break




#############################

if __name__=='__main__':
  import sys, os, datetime, time

  CLIRESPONSE=os.getenv("CLIRESPONSE","1").lower()
  if CLIRESPONSE == "true":
    CLIRESPONSE=1
  if CLIRESPONSE == "false":
    CLIRESPONSE=0
  CLIBase.debug=int(CLIRESPONSE)

  PARAMS=['cmds']
  PARAMSV={'cmds': None}

  if len(sys.argv)<len(PARAMS)+1:
    sys.stderr.write('Usage: %s ' % sys.argv[0])
    for p in PARAMS:
      sys.stderr.write('<%s> ' % p)
    sys.stderr.write('[admin-passwd [cli-host[:port[:conn-type]]]]\n\n')
    sys.stderr.write('where: conn-type could be raw (default) or ssl\n')
    sys.exit(255)
  for i in range(1, len(PARAMS)+1):
    PARAMSV[PARAMS[i-1]]=sys.argv[i]
  if len(sys.argv)>=len(PARAMS)+2:
    if sys.argv[len(PARAMS)+1]:
      CLIPASS=sys.argv[len(PARAMS)+1]
  if len(sys.argv)>=len(PARAMS)+3:
    CLIHOST=sys.argv[len(PARAMS)+2]
  if ':' in CLIHOST:
    try:
      CLIPORT=int(CLIHOST.split(':')[1])
    except ValueError:
      print('Error: Non-numeric CLI port passed as parameter', file=sys.stderr)
      sys.exit(1)
    try:
      CLICONN=CLIHOST.split(':')[2]
    except:
      pass
    CLIHOST=CLIHOST.split(':')[0]

  CLIHOST=os.getenv("CLIHOST",CLIHOST)
  CLIPORT=os.getenv("CLIPORT",CLIPORT)
  CLICONN=os.getenv("CLICONN",CLICONN)
  CLIPASS=os.getenv("CLIPASS",CLIPASS)

  if not CLIPASS:
    import getpass
    while not CLIPASS:
      CLIPASS=getpass.getpass('Enter CLI Admin password:')
      if not CLIPASS:
        print('Empty passwords are not allowed!', file=sys.stderr)

  c=CLIBase(CLIHOST, int(CLIPORT), CLIUSER, CLIPASS, CLICONN)

  if PARAMSV['cmds'] == '-':
    cliCommands = STDIN.read()
  else:
    cliCommands = PARAMSV['cmds']

  c.runCli(cliCommands)

