#!/bin/sh
######################################################################
# 
# PowerChute Network Shutdown Log4Shell Mitigation Script for Linux
# and Virtual Appliance.
#
# Copyright (c) 2022 Schneider Electric, All Rights Reserved. 
#
######################################################################

######################################################################
# Global Constants
######################################################################
LINUX="Linux"
SOLARIS="Solaris"
HPUX="HP-UX"
AIX="AIX"
VIMA="VIMA"
UNKNOWN="UNKNOWN"
XENSERVER="XenServer"
MACOS="MacOS"
x86_64="x86_64"
GROUP1="group1"
GROUP2="group2"
GROUP3="group3"
ARCH="@ARCH@"

TRUE=0
FALSE=1
STR_YES="YES"
YES=0
NO=1
QUIT=2
INVALID=99
PATH=/sbin:$PATH

######################################################################
# Exit Codes
######################################################################
EXIT_SUCCESS=0
EXIT_USAGE=1
EXIT_NOT_ROOT_USER=4
EXIT_UNSUPPORTED_OS=5
EXIT_UPGRADE_NOT_SUPPORTED=6
EXIT_USER_ABORT=7
EXIT_CONFLICT_PCPLUS=8
EXIT_CONFLICT_PCBE=9
EXIT_CONFLICT_PCS=10
EXIT_CONFLICT_VMWARE=11
EXIT_INVALID_INSTALL_DIR=12
EXIT_INVALID_JAVA_VERSION=13
EXIT_INVALID_LOCALE=14
EXIT_ZIPFILE_MISSING=15
EXIT_SILENT_CONFIG_MISSING=16
EXIT_EULA_NOT_ACCEPTED=17
EXIT_SILENT_INSTALL_JAVA_DIR=20
EXIT_TOOLS_MISSING=21

######################################################################
# Global Variables
######################################################################
UPDATE_INSTALL=$FALSE
OS=$UNKNOWN
SILENT_MODE=$FALSE

SRC_DIR=""
INSTALL_DIR=""
APP_DIR=""
JAVA_DIR=""
ACCEPT_EULA="NO"
REGISTER_NMC="NO"
STARTUP=""
SYSV_STARTUP=""
PCBE_STARTUP=""
PCS_STARTUP=""
PCNS_STARTUP=""
OLD_INSTALL_DIR=""
JRE_FILE=""
SPARC=$FALSE
TR="tr"
SYSTEMCTL=$(which systemctl 2>/dev/null)
CHKCONFIG=$(which chkconfig 2>/dev/null)

######################################################################
#  Functions
######################################################################
# trap keyboard interrupt on the following:
# 1 SIGHUP
# 2 SIGINT
# 3 SIGQUIT
# 6 SIGABRT
trap control_c 1 2 3 6

control_c()
# run if user hits control-c
{
  echo -en "\n*** User Abort Detected! Exiting ***\n"
  Echo "Aborting with error code-$EXIT_USER_ABORT"
  CancelAll $EXIT_USER_ABORT
}

# Waits for user key press.
Pause() {
    OLDCONFIG=`stty -g`
    stty -icanon -echo min 1 time 0
    dd count=1 2>/dev/null
    stty $OLDCONFIG
}

Echo() {
    string="$1"
    echo "$string"
}

IsYN() {
    rval=$INVALID
    query_string="$1"
    loop=$TRUE
    while [ $loop -eq $TRUE ]
    do
        Echo ""
        Echo "$query_string "
        read ynq
        case "$ynq" in
        [Yy]*)
            rval=$YES
            loop=$FALSE
            ;;
        [Nn]*)
            rval=$NO
            loop=$FALSE
            ;;
        *)
            Echo "Invalid response."
            ;;
        esac
    done
    return $rval
}

IsRootUser() {
    ROOT="root"
    USER=`id -nu`
    
    if [ $USER != $ROOT ]; then
        Echo "Error: $0 must be run with root privileges!"
        Echo "Aborting with error code-$EXIT_NOT_ROOT_USER"
        exit $EXIT_NOT_ROOT_USER
    fi
}

CheckOS() {
    OS=`uname | grep -i Linux`
    if [ ! -z "$OS" ]; then
        # We're a Linux derivative, decide which one.
        if [ -f /etc/vima-release ] || [ -f /etc/vma-release ]; then
            OS=$VIMA
        else 
            grep XenServer /etc/redhat-release > /dev/null 2>&1
            if [ $? -eq 0 ]; then
                OS=$XENSERVER
            else
                OS=$LINUX
            fi
        fi
    else
        OS=`uname | grep -i HP-UX`
        if [ ! -z "$OS" ] 
        then
            OS=$HPUX
        else
            OS=`uname | grep -i AIX`
            if [ ! -z "$OS" ] 
            then
                OS=$AIX
            else
                OS=`uname | grep -i SOLARIS`
                if [ ! -z "$OS" ]
                then
                    OS=$SOLARIS
                    isSparc
                    SPARC=$?
                else 
                    OS=`uname | grep -i SUNOS`
                    if [ ! -z "$OS" ]
                    then
                        OS=$SOLARIS
                        isSparc
                        SPARC=$?                               
                    else
                        OS=`uname | grep -i DARWIN`
                        if [ ! -z "$OS" ]; then
                            OS=$MACOS
                        else
                            Echo "Aborting with error code-$EXIT_UNSUPPORTED_OS"
                            Echo "Error: Unknown OS."
                            exit $EXIT_UNSUPPORTED_OS
                        fi
                    fi
                fi
            fi
        fi
    fi
    Echo "OS=$OS"
    Echo ""
}

isSparc() {
    rval=$FALSE
    tmp=`uname -a | grep -i SPARC`
    if [ ! -z "$tmp" ] 
    then
        rval=$TRUE
    fi
    return $rval
}

IsPCNSInstalled() {
    upgrade=$FALSE    
    version="Unknown"
    SCRIPT=""
    
    if [ "$OS" = "$MACOS" ]; then
        SCRIPT=/Library/LaunchDaemons/com.apcc.pcns.plist
    else      
        # Check for the SystemD startup script
        if [ -f "$STARTUP" ]; then
            SCRIPT="$STARTUP"
        elif [ -f "$SYSV_STARTUP" ]; then
            SCRIPT="$SYSV_STARTUP"
        fi
    fi
    
    # Echo "SCRIPT=$SCRIPT"
    
    if [ -z "$SCRIPT" ]; then 
        Echo "Could not find PowerChute Network Shutdown installation directory."
        Echo "Aborting with error code-$EXIT_UPGRADE_NOT_SUPPORTED"
        exit $EXIT_UPGRADE_NOT_SUPPORTED
    fi           
}

InitUpdate() {
    # Get PCNS Path
    if [ "$OS" = "$MACOS" ]; then
        OLD_INSTALL_DIR=`defaults read $STARTUP Program`

        # Drop script portion of path...
        OLD_INSTALL_DIR=`dirname $OLD_INSTALL_DIR`

        # Drop group 1 folder portion of path...
        OLD_INSTALL_DIR=`dirname $OLD_INSTALL_DIR`
    else
        if [ -f "$STARTUP" ]; then
            OLD_INSTALL_DIR=`grep '^INSTALL_PATH='  $STARTUP | sed s/INSTALL_PATH=\"// | sed s/\"//`
        elif [ -f "$SYSV_STARTUP" ]; then
            OLD_INSTALL_DIR=`grep '^INSTALL_PATH='  $SYSV_STARTUP | sed s/INSTALL_PATH=\"// | sed s/\"//`
        fi
    fi
    
    Echo "PowerChute Network Shutdown previously installed at: $OLD_INSTALL_DIR"
        
}

SetInstallDir() {
    INSTALL_DIR=`dirname $OLD_INSTALL_DIR`
    
    res=$FALSE
    while [ $res = $FALSE ]
    do
        Echo ""
        Echo "Please enter the installation directory or press enter to install to the default directory ($INSTALL_DIR):"
        read val
        if [ -n "$val" ]; then
             INSTALL_DIR=$val
        fi
        
        # Verify install path
        if [ -n "$INSTALL_DIR" ]; then
            buf=`echo $INSTALL_DIR | grep '^/' ` 
            if [ -z "$buf" ]; then
                Echo "Installation directory must start with \"/\""
                Echo "Aborting with error code-$EXIT_INVALID_INSTALL_DIR"
                exit $EXIT_INVALID_INSTALL_DIR
            fi
            
            buf=`echo $INSTALL_DIR | grep ' ' ` 
            if [ -n "$buf" ]; then
                Echo "Installation directory must not contain white space \" \""
                Echo "Aborting with error code-$EXIT_INVALID_INSTALL_DIR"
                exit $EXIT_INVALID_INSTALL_DIR
            fi
            
            buf=`echo $INSTALL_DIR | grep '\\\' ` 
            if [ -n "$buf" ]; then
                Echo "Installation directory must not contain back slash \"\\\""
                Echo "Aborting with error code-$EXIT_INVALID_INSTALL_DIR"
                exit $EXIT_INVALID_INSTALL_DIR
            fi
            
            PCNS_STARTUP=$INSTALL_DIR/PowerChute/group1/powerchute.sh
            
            if [ ! -f $PCNS_STARTUP ]; then
                Echo "Installation directory does not contain expected files."
                Echo "Aborting with error code-$EXIT_INVALID_INSTALL_DIR"
                exit $EXIT_INVALID_INSTALL_DIR
            fi

            IsYN "Are you sure you want to update PCNS in $INSTALL_DIR/PowerChute [Yes|No]?"
            val=$?
            case $val in
            $YES)
                res=$TRUE                
                ;;
            $NO)
                ;;
            *)
                Echo "Invalid response."
                ;;
            esac
        fi
    done   
}

Initialize() {
    Echo "Initializing ..."
    case "$OS" in
    $VIMA)
        Echo "This version of PowerChute Network Shutdown does not support VMWare ESX or ESXi."
        Echo "Please consult www.apc.com for the required version of PowerChute Network Shutdown."
        Echo "Aborting with error code-$EXIT_UNSUPPORTED_OS"
        CancelAll $EXIT_UNSUPPORTED_OS
        ;;
    $XENSERVER)
        STARTUP=/etc/rc.d/init.d/PowerChute
        PCBE_STARTUP=/etc/rc.d/init.d/PBEAgent
        PCS_STARTUP=/etc/rc.d/init.d/pcs
        ;;
    $LINUX)
        if [ -f /etc/vmware-release ] ; then
            Echo "This version of PowerChute Network Shutdown does not support VMWare ESX or ESXi."
            Echo "Please consult www.apc.com for the required version of PowerChute Network Shutdown."
            Echo "Aborting with error code-$EXIT_UNSUPPORTED_OS"
            CancelAll $EXIT_UNSUPPORTED_OS
        fi
        
        STARTUP=/usr/bin/PowerChute
        SYSV_STARTUP=/etc/init.d/PowerChute
        PCBE_STARTUP=/etc/rc.d/init.d/PBEAgent
        PCS_STARTUP=/etc/rc.d/init.d/pcs
        ;;
    $SOLARIS)
        STARTUP=/etc/rc2.d/S99PowerChute
        PCBE_STARTUP=/etc/rc2.d/S99PBEAgent
        TR=/usr/xpg4/bin/tr
        ;;
    $HPUX)
        STARTUP=/sbin/init.d/pcns
        ;;
    $AIX)
        STARTUP=/etc/rc.APCpcns
        ;;
    $MACOS)
        STARTUP=/Library/LaunchDaemons/com.apcc.pcns.plist
        ;;
    esac
}

CancelAll() {
    cd "$SRC_DIR"
    
    Echo ""
    Echo "Installation cancelled."
    Echo ""
    exit $1
}

verifyTools() {
    Echo "Verifying all necessary tools are available ..."
    ALL_TOOLS=0
    TOOLS="sed"
    for tool in ${TOOLS}; do
        if ! [ -x "$(command -v $tool)" ]; then
            Echo "ERROR: $tool is not available." 1>&2
            ALL_TOOLS=1
        fi
    done

    if [ $ALL_TOOLS -eq 0 ]; then
        Echo "SUCCESS: All necessary tools found."
    else 
        Echo "ERROR: Some necessary tools are not available."
        Echo "Please install the necessary tools and try again."
        Echo "Aborting with error code-$EXIT_TOOLS_MISSING"
        exit $EXIT_TOOLS_MISSING
    fi
}

startService() {
    Echo "Starting service ..."
    
    case "$OS" in       
    $SOLARIS)
        $STARTUP start
        ;;
            
    $MACOS)
        launchctl load -w $STARTUP
        ;;
            
    *)
        # If we have a startup script, use it...
        if [ -f "$STARTUP" ]; then
            $STARTUP start
        elif [ -f "$SYSV_STARTUP" ]; then
            # If we have a SYSV style startup script, use it...
            $SYSV_STARTUP start
        elif [ -n "$SYSTEMCTL" ]; then
            # PCNS 4.2 on Ubuntu 16.04 has systemctl available but doesn't use it.  As such we need ensure use of systemctl comes last.
            $SYSTEMCTL restart PowerChute
        else
            # Will hopefully, never happen. 
            Echo "WARNING: PowerChute Service did not start as expected. Please start the service manually."
        fi        
        ;;
    esac        
}

stopService() {
    Echo "Stopping service ..."
    # Stop daemon
    case "$OS" in
    $VIMA)
        /etc/rc.d/init.d/PowerChute stop
        ;;
    $XENSERVER)
        /etc/rc.d/init.d/PowerChute stop
        ;;
    $LINUX)
        if [ -f /usr/bin/PowerChute ]; then
            /usr/bin/PowerChute stop
        fi
            
        if [ -f /etc/init.d/PowerChute ]; then
            /etc/init.d/PowerChute stop
        fi

        if [ -f /etc/rc.d/init.d/PowerChute ]; then
            /etc/rc.d/init.d/PowerChute stop
        fi
        ;;
    $SOLARIS)
        /etc/rc2.d/S99PowerChute stop
        ;;
    $HPUX)
        /sbin/init.d/pcns stop
        ;;
    $AIX)
        /etc/rc.APCpcns stop
        ;;
    $MACOS)
        launchctl unload $STARTUP > /dev/null 2>&1
        ;;
    esac
}

copyPcnsJar() {
    # Assumes the pcns jar is in the ../files subdirectory, and is called pcns.jar
    Echo "Copying updated pcns.jar..."
    
    TARGET_JAR=$INSTALL_DIR/PowerChute/group1/comp/pcns.jar
    
    cp ./files/pcns.jar $TARGET_JAR
    
    if [ $? -ne 0 ]; then
        Echo "PowerChute jar file could not be copied."
        Echo "Aborting with error code-$EXIT_UPGRADE_NOT_SUPPORTED"
        exit $EXIT_UPGRADE_NOT_SUPPORTED
    fi
}

copyAboutBox() {
    # Assumes the about box is in the ../files subdirectory, and is called about.html
    Echo "Update versioning..."
    TARGET=$INSTALL_DIR/PowerChute/group1/comp/http/html/about.html
    cp ./files/about.html $TARGET
    
    if [ $? -ne 0 ]; then
        Echo "PowerChute about box could not be copied."
        Echo "Aborting with error code-$EXIT_UPGRADE_NOT_SUPPORTED"
        exit $EXIT_UPGRADE_NOT_SUPPORTED
    fi
}

removeOldLog4JLibs() {
    Echo "Remove existing Log4J libraries ..."
    TARGET_DIR=$INSTALL_DIR/PowerChute/group1/lib
    
    rm -f $TARGET_DIR/log4j-api-*.jar && rm -f $TARGET_DIR/log4j-slf4j-*.jar && rm -f $TARGET_DIR/log4j-core-*.jar
    
    if [ $? -ne 0 ]; then
        Echo "Vulnerable Log4j libraries could not be removed."
        Echo "Aborting with error code-$EXIT_UPGRADE_NOT_SUPPORTED"
        exit $EXIT_UPGRADE_NOT_SUPPORTED
    fi
}

copyNewLog4JLibs() {
    Echo "Add updated Log4J libraries ..."
    # Assumes the new log4j libs are in the ../files directory and named log4j-*.jar
    TARGET_DIR=$INSTALL_DIR/PowerChute/group1/lib
    
    cp ./files/log4j-*.jar $TARGET_DIR
     
    if [ $? -ne 0 ]; then
        Echo "Log4j libraries could not be copied to PowerCHute libs folder."
        Echo "Aborting with error code-$EXIT_UPGRADE_NOT_SUPPORTED"
        exit $EXIT_UPGRADE_NOT_SUPPORTED
    fi
}

patchWithBsdSed() {
    # BSD sed doesn't have an in-place option...         
    PATCH_FILE="$PCNS_STARTUP.patch"
    
    # Handle Log4j 2.10.0 to 2.17.1 ...
    sed -e 's/log4j-api-2.10.0/log4j-api-2.17.1/g' $PCNS_STARTUP > $PATCH_FILE
    mv $PATCH_FILE $PCNS_STARTUP
            
    sed -e 's/log4j-core-2.10.0/log4j-core-2.17.1/g' $PCNS_STARTUP > $PATCH_FILE
    mv $PATCH_FILE $PCNS_STARTUP
        
    sed -e 's/log4j-slf4j-impl-2.10.0/log4j-slf4j-impl-2.17.1/g' $PCNS_STARTUP > $PATCH_FILE
    mv $PATCH_FILE $PCNS_STARTUP
    
    # Handle Log4j 2.2 to 2.17.1 ...
    sed -e 's/log4j-api-2.2/log4j-api-2.17.1/g' $PCNS_STARTUP > $PATCH_FILE
    mv $PATCH_FILE $PCNS_STARTUP
            
    sed -e 's/log4j-core-2.2/log4j-core-2.17.1/g' $PCNS_STARTUP > $PATCH_FILE
    mv $PATCH_FILE $PCNS_STARTUP
        
    sed -e 's/log4j-slf4j-impl-2.2/log4j-slf4j-impl-2.17.1/g' $PCNS_STARTUP > $PATCH_FILE
    mv $PATCH_FILE $PCNS_STARTUP
        
    chmod 744 $PCNS_STARTUP
}

patchWithGnuSed() {
    sed -i -e 's/log4j-api-2.2/log4j-api-2.17.1/g' $PCNS_STARTUP
    sed -i -e 's/log4j-core-2.2/log4j-core-2.17.1/g' $PCNS_STARTUP
    sed -i -e 's/log4j-slf4j-impl-2.2/log4j-slf4j-impl-2.17.1/g' $PCNS_STARTUP
}

modifyPowerChuteClasspath() {
    grep log4j-core $PCNS_STARTUP > /dev/null 2>&1
    
     if [ $? -eq 0 ]; then
        Echo "Modifying classpath ..."
    
        case "$OS" in     
        $AIX)
            patchWithBsdSed
            ;;
              
        $SOLARIS)
            patchWithBsdSed
            ;;
            
        $MACOS)
            patchWithBsdSed
            ;;
            
        *)
            # Default/ Linux uses GNU sed...            
            patchWithGnuSed
            ;;
        esac
    
    fi
}

######################################################################
# Main routine
######################################################################
SRC_DIR=`pwd`

# Must be root to run this script...
IsRootUser

# Script supports Linux/ Virtual appliance only...
CheckOS

# Verify we have the necessary tools...
verifyTools

# Detect startup scripts...
Initialize

# Verify that PCNS is installed...
IsPCNSInstalled

# Find the install dir ...
InitUpdate

# Confirm and set install dir with user...
SetInstallDir

# Stop the service...
stopService

# Apply patch to startup script
Echo "Applying patch ..."

# Copy new pcns.jar into place
copyPcnsJar
copyAboutBox

# Remove old log4j libs
removeOldLog4JLibs

# Copy new log4j libs into place
copyNewLog4JLibs

# Edit the classpath (for non-wildcard classpaths)
modifyPowerChuteClasspath

startService

Echo "Log4Shell patch applied to PowerChute."
cd "$SRC_DIR"
exit $EXIT_SUCCESS
