VNCpilot 1.0.1 Source

Version 5
Visibility: Open to anyone

    This is the source code for VNCpilot 1.0.1

     

    To install VNCpilot please see: VNCpilot for OL/EL 7

    Documentation is available at: VNCpilot - Simple And Secure Remote Access

     

    #!/bin/bash
    # ----------------------------------------------------------------------------
    # Author: Dude! @ Oracle Community, August 2017
    # File: /usr/local/bin/vncpilot
    # Version: 1.0.1
    #
    # Purpose:
    #  Provides a turnkey solution to create or delete secure VNC access ad hoc
    #  without the need to install X Window or to understand X remote access.
    #  The program creates and manages a systemd service unit based on TigerVNC.
    #  It is ideal for running remote X applications, such as the Oracle Universal
    #  Installer or DBA tools on demand. Only VNC viewer connections addressed 
    #  to localhost are allowed. Remote connections can be established through
    #  a secure SSH tunnel. VNCpilot provides all necessary information.
    #
    # Requirements:
    #  RHEL 7 or derivatives.
    #
    # License:
    #  Copyright (C) 2018 Dude! @ Oracle Community.
    #
    #  This program is free software: you can redistribute it and/or modify
    #  it under the terms of the GNU General Public License version 3 as
    #  published by the Free Software Foundation.
    #
    #  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/>.
    #
    # History:
    #  * Tue Jun 12 2018 Dude! <otn.dude@gmail.com>
    #  - Initial release 1.0.
    #  * Thu Sep 13 2018 Dude! <otn.dude@gmail.com>
    #  - Version 1.0.1.
    #    Change vncpilot summary screen for better understanding.
    # ----------------------------------------------------------------------------
    
    ### Debug
    
    [ "${verify:-0}" -eq 1 ] && set -x  # export verify=1 | unset verify
    
    ### Variables
    
    g_nam=(VNCpilot vncpilot 1.0.1)
    g_cmd="grep read id tr head tail fold useradd userdel cp true cat
           pkill pgrep date echo paste chpasswd infocmp tput grep rpm pwd
           xargs cut sed eval rm systemctl ip ss chmod chown date mkdir
           vncserver gnome-terminal xclock vncpasswd runuser"
    g_rel=/etc/redhat-release
    g_invc=':;,!@#$%^&*()?{}[]|~'
    g_puid=/sys/class/dmi/id/product_uuid
    g_opt=( 1024x768 1280x800 1680x1050 1280x1024 1440x900 1600x1200
            1680x1050 1920x1080 1920x1200 )
    g_shr=( -nevershared -alwaysshared )
    g_port=95 g_vnc2=16 g_vnc3=1024x768
    g_tmpl="/lib/systemd/system/vncserver@.service"
    g_exec=( 'ExecStart=/usr/sbin/runuser -l <USER> -c "/usr/bin/vncserver %i"'
             'ExecStart=/usr/bin/vncserver %i' )
    g_unit="/etc/systemd/system/vncserver@:${g_port}.service"
    g_vnc4="-localhost -SecurityTypes=vncauth"
    g_ref="https://community.oracle.com/docs/DOC-1024831"
    g_lic="Free under the GNU General Public License"
    
    ### Action
    
    e() { \echo "$@"; }
    p() { \printf "$@"; }
    
    p "%s %s. Copyright (c) 2018 Dude! @ Oracle Community.\n" \
      ${g_nam[0]} ${g_nam[2]}
    e "${g_lic}. See ${g_nam[1]} --help for info."  
    
    msg() {
      # Optional: $1, $2, $3.
      case "$1" in
        E) e "%${g_nam[1]}-E-${2:-ERROR}, $3"; e $'\007' ;;
        X) e "%${g_nam[1]}-X-${2:-EXIT}, $3"; e ;;
        W) e "%${g_nam[1]}-W-${2:-WARNING}, $3" ;;
        I) e "%${g_nam[1]}-I-${2:-INFO}, $3" ;;
        *) e "$@" ;;
      esac
    }
    
    # Preliminary exit when requriements are not met.
    [ ${BASH_VERSINFO:-0} -ge 4 ] \
      || { e; msg E ${LINENO} "Bash version 4 or later required."; exit 1; }
    
    c=( $(\cksum <<< $(\head -n -1 ${BASH_SOURCE[0]})) )
    d=( $(\tail -1 ${BASH_SOURCE[0]} | \grep ^#) )
    [ "${c[0]}" = "${d[2]}" ] && unset c d \
      || { e; msg E ${LINENO} "self-integrity check failed."; exit 1; }
    
    if [ "$0" = -bash ]; then
      e; msg E ${LINENO} "source executing not supported."; exit 1
    else
      shopt -s extglob   # needs to be defined outside function.
      IFS=$' \t\n'       # standard internal field seperators.
    fi
    
    [ $(/usr/bin/id -u) -ne 0 ] \
      && { e; msg E ${LINENO} "insufficient privileges."
            msg "For help: <$g_ref>"; e; exit 1; }
    
    [ -f "$g_rel" ] \
      || { e; msg E ${LINENO} "unsupported OS distribution."; exit 1; }
    
    s=$(/bin/ls -l /proc/1/exe); s=${s##*/}
    [ $s = systemd ] \
      || { e; msg E ${LINENO} "init process is not systemd."; exit 1; }
    
    f_chkcmd() {
      # Check and set command availablity.
      local item
      for item in $1; do
        hash $item 2>&- \
        && { eval ${item//-/_}=$item; } \
        || { e; msg E ${LINENO} "$item: no such command."; exit 1; }; done
    }
    
    f_chkcmd "$g_cmd"
    
    f_vtseq() {
      # Define terminal sequences (if available).
      local l_a l_b l_i l_ret=0
      l_a="bold= sgr= setaf= rev="
      l_b=$($infocmp screen 2>&-)
      for l_i in $a; do [ "$($grep -o $l_i <<<"$b")" ] || l_ret=1; done
      [ "$l_ret" -eq 0 ] \
        && { bo=$($tput bold); rg=$($tput bel); rv=$($tput rev)
             rd=$($tput setaf 1); gn=$($tput setaf 2); no=$($tput sgr0); }
    }; f_vtseq; unset -f f_vtseq
    
    f_yna() {
      # Requries $1, Optional $2.
      local l_s l_a
      [ "$2" ] && { e; e "$2"; e; }
      l_s="${rg}Enter (${bo}Y${no})es or (${bo}N${no})o, or (${bo}A${no})bort"
      case "$1" in # default answer.
        [Yy]) l_s="$l_s: [y] " ;;
        [Nn]) l_s="$l_s: [n] " ;;
      esac
      $read -r -e -n 10 -p "$l_s" l_a
      [ -z "$l_a" ] && l_a="$1" # Default.
      case "$l_a" in
        [Yy]*) return 0 ;;
        [Nn]*) return 1 ;;
        [Aa]*) return 3 ;;
            *) e; msg E ${LINENO} "invalid input."; exit 1 ;;
      esac
    }
    
    f_try() {
      case $1 in
        create) msg "Try '${g_nam[1]} --create', or '${g_nam[1]} --status'." ;;
        delete) msg "Try '${g_nam[1]} --delete', or '${g_nam[1]} --status'." ;;
        help)   msg "Try '${g_nam[1]} --help'." ;;
      esac
    }
    
    f_chktmpl() {
      # Verify vncserver template.
      local l_a l_item l_ret=0
      for l_item in "${g_exec[@]}"; do
        l_a=$($grep "$l_item" $g_tmpl | $grep -v '^#')
        [ -z "$l_a" ] || l_ret=1
      done
        if [ $l_ret -ne 1 ]; then
          e; msg W ${LINENO} "unkown vncservice template."
          f_yna y " ${bo}Do you wish to abort?${no}"
          [ $? -eq 1 ] || { e; msg X ${LINENO} "user canceled."; exit 0; }
        fi
    }
    
    f_chktcp() {
      local l_ret
      # Redirect error output and check if TCP port is available
      $ss -ntl | $grep -w 59$g_port > /dev/null \
        && { e; msg E ${LINENO} "TCP port 59$g_port already in use."; exit 1; }
    }
    
    f_uget() {
      # Generates g_user array.
      [ -e $g_puid ] || { e; msg E ${LINENO} "$g_puid not found."; exit 1; }
      # User first 3 characters of product_uuid to construct g_user.
      $read -n 3 <$g_puid; g_user="vnc-$REPLY"
      # Check if user exists and set VNC_ACCT array.
      $id -u $g_user &>/dev/null
      if [ $? -eq 0 ]; then
        g_user[1]=1
        g_user[2]=$($grep $g_user /etc/passwd | $cut -d: -f6)
      else
        g_user=( $g_user 0 "n/a." )
      fi
    }
    
    f_uadd() {
      f_uget; local a
      $useradd $g_user &>/dev/null
      case "$?" in
        0) # Generate a random password (???-???-???)
           g_user[3]=$($tr -dc 3-9A-Za-z </dev/urandom | $head -c9; $echo)
           g_user[3]=$($echo ${g_user[3]} | $fold -w3 | $paste -sd'-' -)
           $echo "$g_user:${g_user[3]}" | $chpasswd ;;
        9) e; msg W ${LINENO} "user $g_user already exists."
           e; f_try delete; exit 1 ;;
        *) e; msg E ${LINENO} "user $g_user cannot be created."; exit 1 ;;
      esac
    }
    
    f_vpass() {
      f_uget; local l_a l_b
      # Must create VNC password file before starting service.
      l_a=${g_user[2]}/.vnc; l_b=$l_a/passwd
      $mkdir -p -m 775 $l_a && $chown $g_user:$g_user $l_a
      $vncpasswd -f <<<${g_user[3]} >$l_b \
        && $chmod 600 $l_b && $chown $g_user:$g_user $l_b
      [ -e $l_b ] || { e; msg E ${LINENO} "cannot create VNC password"; exit 1; }
    } 
    
    f_vxstart() {
      f_uget; local l_item l_a l_b l_c
      # Must create xstartup before starting service.
      l_a=${g_user[2]}/.vnc; l_b=$l_a/xstartup
      l_c="--geometry 80x24+50+50"
      for l_item in "#!/bin/sh" "unset SESSION_MANAGER" \
        "unset DBUS_SESSION_BUS_ADDRESS" ". /etc/X11/xinit/xinitrc-common" \
        "[ -x /usr/bin/xsetroot ] && /usr/bin/xsetroot -solid '#00969A'" \
        "[ -x /usr/bin/xclock ] && /usr/bin/xclock -geometry 100x100-5+5 &" \
        "[ -x /usr/bin/gnome-terminal ] && /usr/bin/gnome-terminal $l_c & " \
        "[ -x /usr/bin/metacity ] && /usr/bin/metacity &"; do
        e "$l_item" >>$l_b; done  
      $chmod 755 $l_b && chown $g_user:$g_user $l_b
      [ -e $l_b ] || { e; msg E ${LINENO} "cannot create $l_b"; exit 1; }
    }
    
    f_vcreate() {
      local l_a
      if [ ${g_unit[1]} -eq 1 ]; then
        e; msg E ${LINENO} "service unit '${g_unit##*/}' already exists."
        f_try delete; exit 1
      fi
      $cp $g_tmpl $g_unit \
        || { e; msg E ${LINENO} "cannot copy $g_unit."; exit 1; }
      $sed -i '/^#/!s/<USER>/'"$g_user"'/g' $g_unit
      l_a="$g_vnc1 -depth $g_vnc2 -geometry $g_vnc3"
      l_a="$l_a $g_vnc4"
      $sed -i '/^#/!s/vncserver %i/vncserver %i '"$l_a"'/g' $g_unit
    }
    
    f_vdelete() {
      f_uget; e
      if [ ${g_user[1]} -eq 1 ]; then
        p " %-20s ${bo}%s${no}\n" "VNC service:" ${bo}${g_unit##*/}
        p " %-20s ${bo}%s${no}\n" "Username:" $g_user
        p " %-20s ${bo}%s${no}\n" "Directory:" ${g_user[2]}; e
        e " WARNING: All corresponding VNC viewer connections and processes"
        e "          will be aborted. The VNC service, VNC user account and"
        e "          user home directory will be be erased."
        f_yna n " ${bo}Are you sure you want to remove the VNC service?${no}" || \
          { e; msg X ${LINENO} "user canceled."; exit 0; }
      else
        msg I ${LINENO} "user account '$g_user' does not exist."
      fi
      $pkill -u $g_user &>/dev/null
      $systemctl stop ${g_unit##*/} &>/dev/null
      $systemctl disable ${g_unit##*/} &>/dev/null &>/dev/null
      # Remove lock file in TMPDIR or /tmp
      $rm -f ${TMPDIR-/tmp}/.X$g_port-lock ${TMPDIR-/tmp}/.X11-unix/X$g_port
      # Stop vncserver before removing system account.
      [ ${g_user[1]} -eq 1 ] && $userdel -fr $g_user &>/dev/null
      [ ${g_unit[1]} -eq 0 ] \
        && msg I ${LINENO} "service unit '${g_unit##*/}' does not exist." \
        || $rm -f $g_unit
    }
    
    f_vreload() {
      if [ ${g_unit[1]} -eq 0 ]; then
        e; msg E ${LINENO} "service unit '${g_unit##*/}' does not exist."
        f_try create; exit 1
      fi
      f_uget 
      if [ $reload -eq 1 ]; then
        e; p " %-20s ${bo}%s${no}\n" "VNC service:" ${bo}${g_unit##*/}
        p " %-20s ${bo}%s${no}\n" "Username:" $g_user; e
        e " WARNING: All corresponding VNC client connections and processes"
        e "          will be aborted."
        f_yna n " ${bo}Are you sure you want to reload the VNC service?${no}" || \
          { e; msg X ${LINENO} "user canceled."; exit 0; }
        $pkill -u $g_user &>/dev/null 
      fi
      $systemctl enable ${g_unit##*/} &>/dev/null
      $systemctl daemon-reload
      $systemctl restart ${g_unit##*/}
    }
    
    f_ustat() {
      f_uget
      local l_a l_b l_c l_d
      # Custom columns.
      l_b="| Username"; l_a="       ";
      [ ${g_user[1]} -eq 1 ] && l_c="| ${bo}$g_user${no} " || l_c="|   n/a.   "
      [ $create -eq 1 ] \
        && { l_b="$l_b | Password"; l_c="$l_c | ${bo}${g_user[3]}${no}"; l_a=" "; }
      [ $reload -eq 1 ] && { l_b=""; l_c=""; }
      g_ip=$($ip -4 route get 255.255.255.255 | $head -1 | $cut -d' ' -f6)
      l_d=$($true &>/dev/null </dev/tcp/127.0.0.1/59$g_port \
          && { e " ${gn}listening${no} "; } || { e "  ${rd}denied${no}   "; })
      p "%s%-15s | %-8s | %-11s %s\n" \
        "$l_a" "TCP/IP address" "TCP port" "Port status" "$l_b"
      p "%s${bo}%-15s${no} | ${bo}%-8s${no} | %-11s %s\n" \
        "$l_a" $g_ip "  59$g_port  " "$l_d" "$l_c"
    }
    
    f_vstat() {
      local l_a l_item l_arg
      e " ${bo}Service Summary${no}:"; e
      p " %-20s %s\n" "VNC service unit:" \
        $([ ${g_unit[1]} -eq 1 ] && $echo $g_unit || $echo "n/a." )
      # Determine Xvnc process attributes.
      if [ ${g_unit[1]} -eq 1 ]; then
        # Get cmdline and match certain elements.
        l_arg=$($pgrep -f "rfbport 59$g_port")
        l_arg=$($xargs -0 < /proc/$l_arg/cmdline)
        for l_item in "${g_shr[@]}"; do
          l_a="$l_a $($grep -ow -- $l_item <<<$l_arg)"
        done   
        l_a="$l_a $($grep -Eio 'geometry(\s+[^\s]+){1}' <<<$l_arg \
             | $cut -d ' ' -f 2)x$g_vnc2"
        l_a="$l_a $($grep -ow -- '-localhost' <<<$l_arg)"
        l_a="$l_a $($grep -iow 'Security.*' <<<$l_arg | $cut -d '=' -f 2)"    
        # Remove dashes, trim spaces and separate by comma.
        l_a=${l_a//-/}; l_a=$(echo $l_a); l_a=${l_a// /, } 
        p " %-20s %s\n" "VNC attributes:" "$l_a"
      fi
      if [ ${g_unit[1]} -eq 1 ]; then
        p " %-20s %s, ${bo}%s${no}\n" \
          "Service status:" "${g_unit[2]}" "${g_unit[3]}"
      else
        p " %-20s %s\n" "Service status:" "${g_unit[2]}"
      fi; e
    }
    
    f_connect() {
      local l_a="${bo}Note${no}:"; e "
      $l_a  1. Use SSH on your PC to forward any local connection to
                TCP port 5901 to the remote Linux VNC service at 59$g_port:
                ${bo}ssh -L 5901:localhost:59$g_port $g_user@$g_ip${no}"; e "
             2. Open the VNC viewer on your PC Desktop and connect to:
                ${bo}localhost:5901${no}"; e "
     For help: <$g_ref>"
    }
    
    f_sysd() {
      # Generate g_unit array.
      if [ -e $g_unit ]; then
        g_unit[1]=1
        g_unit[2]=$($systemctl is-enabled ${g_unit##*/})
        g_unit[3]=$($systemctl is-active ${g_unit##*/})
      else
        g_unit=( $g_unit 0 "n/a." "n/a." )
      fi
      case $1 in
        create) f_wait; f_vcreate ;;
        delete) f_vdelete ;;
        reload) f_vreload ;;
        status) f_vstat ;;
      esac
    }
    
    f_argerr() {
      case $1 in
        2) e; msg E ${LINENO} "invalid command line argument." ;;
        3) e; msg E ${LINENO} "invalid combination of arguments." ;;
        4) e; msg E ${LINENO} "invalid screen resolution." ;;
        6) e; msg E ${LINENO} "invalid character in command line argument." ;;
      esac
      f_try help; exit 1
    }
    
    f_argcreate() {
      # Process and set optional parameters g_vnc1, g_vnc3.
      local l_a=$g_rem l_geo l_shr
      # Match -size parameter. Allow spaces between = sign.
      l_geo=$($grep -o -w -- '-size\s*=\s*[0-9]\{3,4\}x[0-9]\{3,4\}' <<<$l_a)
      if [ "$l_geo" ]; then
        g_rem=${g_rem/$l_geo} # Remove match from string.
        # Remove spaces and extract geometry value.
        l_geo=$($tr -d ' ' <<<$l_geo); l_geo=${l_geo#-size=}
        # Compare with valid optins.
        [ "$($grep -o $l_geo <<<${g_opt[*]})" ] \
          && g_vnc3=$l_geo || f_argerr 4
      fi
      # If screen size was specified, check if valid.
      [ "$($grep -o -- -size <<<$l_a)" -a -z $l_geo ] && f_argerr 4
      # Match shared parameter.
      l_shr=$($grep -o -w -- '-shared' <<<$l_a)
      [ "$l_shr" ] \
        && { g_rem=${g_rem/$l_shr}; g_vnc1=${g_shr[1]}; } || g_vnc1=$g_shr
    }
    
    f_argchk1() {
      # Validate first command line argument.
      local l_a l_i
      create=0 delete=0 status=0 reload=0 help=0 
      f() { local l_i l_ret=0
        for l_i in $1; do
          # Check if valid argument and remove match from g_rem.
          [ "$($grep -i -w -- "$l_i" <<<"$2")" = "$l_i" ] \
            && { l_ret=1; g_rem=${g_rem/$l_i}; }
        done; [ $l_ret -eq 1 ] && return 1 | return 0; }
      f '-c --create' $1 && create=1
      f '-d --delete' $1 && delete=1
      f '-s --status' $1 && status=1
      f '-r --reload' $1 && reload=1
      f '-h --help' $1 && help=1
    }
    
    f_wait() { p "\n Please wait ...\r"; }
    
    f_anykey() {
      p "                  ${rv} Hit any key to continue - q to quit ${no}"
      $read -n 1 -s
      p "\r                                                       \r"
      REPLY=$($tr '[:upper:]' '[:lower:]' <<<$REPLY)
      [ "$REPLY" = 'q' ] && { e; exit 0; }
    }
    
    f_usage() {
      e; e " ${bo}Usage${no}:   ${g_nam[1]} [ command ]"; e "
      Commands:  -c, --create [ options ]    Create a new a VNC service.
                 -d, --delete                Remove the VNC service.
                 -s, --status                Show the status of the VNC service.
                 -r, --reload                Restart the VNC service.
                 -h, --help                  Display this help and exit."; e "
      Create options (optional):"; e "
       -shared            Support VNC session sharing.
       -size=<#>x<#>      Screen geometry in pixels."; e '
      Screen size   15" or less   16" - 19"   20" - 22"   23" or more     HDTV
      Classic        1024x768     1280x1024   1600x1200
      Widescreen     1280x800     1440x900    1680x1050    1920x1200    1920x1080'
      e; e "    Example: ${g_nam[1]} --create -shared -size=1280x800"
      e; f_anykey; e " ${bo}Note${no}:"; e "
      VNC session sharing is not enabled by default. Any subsequent VNC client
      connection will seamlessly resume the existing VNC session and disconnect
      the previous client. In order to allow multiple VNC clients to share a VNC
      session, add the -shared parameter when creating the VNC service."; e " 
      The default VNC screen size is 1024 by 768 pixels. To support a different
      screen size, specify the -size parameter"; e "
      The VNC service will only accept local connections. To establish a VNC
      client connection from another computer, use SSH to create a secure tunnel
      to forward the local VNC client connection port to the TCP port of the
      remote VNC server."; e "
      ${g_nam[0]} creates a regular user account with a machine specific user name
      and system generated VNC password, which can also be used to establish the
      initial VNC connection. For instructions how to connect and authenticate,
      please see the displayed Service Summary."; e; e
      e; f_anykey; e " ${bo}Copyright and Disclaimer${no}:"; e "
      Copyright (c) 2018 Dude! @ Oracle Community."; e "
      This program is free software and distributed under the terms of the GNU
      General Public License version 3. Please use at your own risk."; e "
      See <https://www.gnu.org/licenses/> for licensing information."; e; e "
      For help and other information, please visit:"; e "
      <$g_ref>."; e; e
      e "  Thanks for using ${g_nam[0]} and best of luck."; e
      e "  Dude!"; e; e; e; e
    }
    
    ### Main
    
    [ $# -eq 0 ] && { e; f_argerr; }
    # Check for invalid chars.
    a=$($tr -d "$g_invc" <<<$*)
    [ "$a" != "$*" ] && f_argerr 6
    # Convert all command line arguments to lowercase.
    a=$($tr '[:upper:]' '[:lower:]' <<<$*); g_rem=$a
    a=( $a ) # Convert $a to array.
    f_argchk1 ${a[@]}
    # Check for invalid argument combinations.
    [ $((create+delete+status+reload+help)) -ne 1 ] \
      && f_argerr 2
    # Only create can have more than one argument.
    [ $((delete+status+reload+help)) -eq 1 -a $# -gt 1 ] \
        && f_argerr 3
    # Check optional create parameters.
    [ $create -eq 1 ] && f_argcreate "${a[@]}"
    # Check for illegal parameters.
    g_rem=$($tr -d ' ' <<<$g_rem); [ "$g_rem" ] && f_argerr 2
    
    [ $help -eq 1 ] && action=help
    [ $create -eq 1 ] && action=create
    [ $delete -eq 1 ] && action=delete
    [ $status -eq 1 ] && action=status
    [ $reload -eq 1 ] && action=reload
    
    case $action in
      help)      f_usage; exit 0 ;;
      create)    f_chktmpl; f_chktcp; f_uadd; f_vpass; f_vxstart; f_sysd create
                 f_sysd reload; f_sysd status; f_ustat; e; f_connect; e ;;
      delete)    f_sysd delete; e; f_sysd status; f_ustat; e ;;
      reload)    f_sysd reload; e; f_sysd status; f_ustat; e ;;
      status)    e; f_sysd status; f_ustat; e ;;
    esac
    
    # EOF 3649754024