3
\$\begingroup\$

This is an mqtt driven, interactive, and asynchronous remote shell daemon written in bash. I call it "MqSH" or simply "mq". It works with bash, the busybox ash shell, and perhaps other Bourne-like shells. It's designed for embedded devices with low system resource, so I wrote to be friendly to busybox-based systems.

It uses the mosquitto_sub (or subclient) program to subscribe to a topic (ex. shell/input), and than uses mosquitto_pub (pubclient) to publishe the output of the received command to another topic (ex. data/output). Commands and ouput are sent to/from the client encoded in base64. To allow the program to be used as an API, the output of the commands are formatted into json before they are published, unless the command is prefixed with _SH_.

Here's an example of what happens when you run the script in debug mode:

mqsh in left pane, handler in right

I've tried to reduce dependencies as much as possible, but it does still depend on a few programs to run that you may don't have on your system. Those are jshon (a command line json formatter), pubclient and subclient (mosquitto_sub/pub c programs), and some stuff found in GNU coreutils, (base64, mktemp, sleep, echo), and shell with printf and wait. (I also have a fork of busybox with everything this script needs to run included as applets here).

Basically, I am looking for any constructive criticism anyone can give me, but more specifically, I would like to know if the program is daemonizing properly.

I used to run it with:

nohup bash mq

but now I start it with:

start-stop-daemon -p /var/run/mq.pid -S --exec bash /path/to/mq

and the script seems to daemonize correctly (but only if ran as root) (correction: it does get inherited by init even if not ran root), and the process gets disowned and inherited by init. Edit: despite that, sometimes it seem to die for no apparent reason. It may be network conditions (broken tcp connection, etc), but I would like someone to review this code to see if I am missing anything.

I would use the bash builtin disown command, but since the ash shell does not have disown, and I want it to be busybox compliant, that is not an option.

Is there anything else I should do improve the daemon functionality? If I recall correctly, to daemonize, a program should redirect stdout, stderr, and stdin to a file, ignore SIGHUP, fork so that's inherited by init, not have a shebang if it's a shell script, and also change to directory /. I believe that I am doing all but the latter, because in this use case, if the working directory ever unmounted, the program would die anyway, as it depends on files and a fifo in that directory to function.

#            __   __                 
#      |\/| /  \ /__` |__|           
#      |  | \__X .__/ |  |  

jsonify(){
# jsonify function, called by publish()
# turns command output into json. a little hacky, but works
jtemp=$(mktemp json.XXXXXX);rm $jtemp
tempf="$workdir/$jtemp"
input="$1"
echo "$2" > $tempf
bbj="$(jshon -Qs 2>/dev/null 1>/dev/null)" # calls jshon
getip=$bbj"$(echo $mq_ip)"
unixtime=$bbj"$(date +%s)"
getdate=$bbj"$(date)"
getuptime=$bbj"$(uptime | sed s/\,//)"
kernelcmdline=$bbj"$(cat /proc/cmdline)"
getid=$bbj"$(id)"
kcrypto=$bbj"$(cat /proc/crypto | grep name | cut -d':' -f2 | uniq | tr -s '\n' ' '|sed s/\,//)"
getversion=$bbj"$(cat /proc/version)"
memstat=$bbj"$(cat /proc/meminfo | head -n 3 | tr -s '\n' ' ')"
getcwd=$bbj"$(pwd)"
defshell=$bbj"$(echo $SHELL)"
getstty=$bbj"$(stty)"
term=$bbj"$(echo $TERM)"
cpuname=$bbj"$(cat /proc/cpuinfo | grep name)"
status=$bbj"$(if ([ -s /tmp/.status ] && [ -f /tmp/.status ]); then echo "SYSTEM_BUSY" ; else echo "SYSTEM_READY";fi)" # for future uses
hashcmd=$bbj"$(md5sum $tempf |head -c 32)"
uuid=$bbj"'$unixtime.$hashcmd'"
botversion=$bbj"'$(echo $mq_version)'"
output=$bbj"'$($shell $tempf)'"
cmdline=$bbj"$(echo $(cat $input))"

echo '{
"ip" : "'$getip'",
"unixtime": "'$unixtime'",
"date": "'$getdate'",
"uptime" : "'$getuptime'",
"cpuname": "'$cpuname'",
"memstat" : "'$memstat'",
"id" : "'$getid'",
"version" : "'$getversion'",
"kernel_cmdline": "'$kernelcmdline'",
"kernel_crypto": "'$kcrypto'",
"default shell": "'$defshell'",
"current shell": "'$shell'",
"term": "'$term'",
"stty": "'$getstty'",
"cwd": "'$getcwd'",
"uuid": "'$uuid'",
"status": "'$status'",
"bot version: "'$botversion'",
"cmdline": "'$cmdline'",
"output": "'$output'"
}' 2>>$errorlog
rm -f $jtemp
}

publish(){
# publish function, called by execute() 
uxt=$(date +%s)
pubtemp=$(mktemp pub.XXXXXX);rm $pubtemp
jshout=$workdir/jshout.$uxt
jshin=$workdir/$pubtemp
[ $mq_debug == "1" ] && echo "[<<] Publishing output to topic $mq_pubtop on $mq_host"
echo "$input" >$jshin
(jsonify "$jshin" "cat $out" | base64) 2>> $errorlog > $jshout # call jsonify function to turn output into json
if ([[ "$?" -eq "0" ]] && [[ -s $out ]]); then
  pubclient -h $mq_host -i ${mq_ip} -q 0 -t "data/$mq_pubtop" -u bot -P $mq_pass -f $jshout # finally publish the file
fi
rm -f $out $jshout $jshin 2>>$errorlog
}

execute(){ 
# execute function, called from run(), execs a command, jsonify output, and publish
input="$@"
uxt=$(date +%s)
temp=$(mktemp cmd.XXXXXX);rm $temp # how thef do i declare this without immediatly creating the file?
piddir=/tmp/.mqsh/pids
case "$@" in *)
_cmd_="${workdir}/${temp}"
printf 'export thrdpid=$(mktemp XXXXXX);rm $thrdpid \necho "$$" > /tmp/.mqsh/pids/${thrdpid}.pid \n' >$_cmd_
echo $@ >> $_cmd_
out="$workdir/output.${uxt}"
((($shell $_cmd_) ;wait) 2>> $errorlog > $out );\
((if [[ -s $out ]] ; then publish $out "$input"; else rm $out;fi &)&)
for i in `ls $piddir`;do if ! ps|grep `cat $piddir/$i`|grep -v 'grep' >/dev/null 2>&1;then rm $piddir/$i;fi ;done
rm -f $_cmd_
esac
[ $mq_debug == "1" ] && echo '[<<] Executed command in a thread'
} 


repoGet(){
# get binares/scripts/files from $mq_httphost and check sha1sums
checkhash(){
  bin="$1";fhash="$2"
  if (echo $fhash *$bin | sha1sum -c -) > /dev/null 2>&1 ; then
    return 0
  else 
    return 1
  fi }

getbin="$1"

[ $mq_debug == "1" ] && echo "Getting hash file of $1"
wget -O /tmp/bin.sha1 $mq_httphost/repo/$getbin.sha1
gethash=$(cat /tmp/bin.sha1)
if ! checkhash $mq_binpath/$getbin $gethash;then 
  rm /tmp/bin.out; 
  for i in $(seq 1 5) ;do 
    [ $mq_debug == "1" ] && echo "Downloading $getbin from repository..."
    wget -O /tmp/bin.out $mq_httphost/repo/$getbin; 
    if checkhash /tmp/bin.out $gethash;then  
      [[ $mq_debug == "1" ]] && echo "Hash matches!"
      break
    else 
      [[ $mq_debug == "1" ]] && echo "Corrupted file, wait and try again..."
      sleep $(echo $RANDOM|head -c 2)
    fi
  done 
mv /tmp/bin.out $mq_binpath/$getbin
chmod +x $mq_binpath/$getbin
fi
}

clearmem(){ 
# clear any unecessary files we created
>$workdir/error.log 
rm -f $workdir/cmd.* $workdir/output* $workdir/json*
>$cwd/nohup.out
}

ctrl_c(){ 
#cleanup/exit debug mode
echo -en "\n## Caught SIGINT; Clean up and Exit \n"
kill `cat $subpidfile`
rm $pipe $enc $denc $workdir/output* $pidfile $subpidfile
unset mq_host mq_pass mq_path mq_debug key mq_intf mq_subtop mq_pubtop mq_httphost mq_ipthost mq_binpath shell
exit
}

quit(){ 
# cleanup/exit daemon mode
(rm $pipe $enc $denc $workdir/output*
kill `cat $subpidfile` && rm $subpidfile
unset mq_host mq_pass mq_path mq_debug key mq_intf mq_subtop mq_pubtop mq_httphost mq_ipthost mq_binpath shell
[ $mq_debug == "0" ] &&\
reboot || (rm -f $workdir);exit)
}

update(){ 
# this doesn't quite work as expected. it should update this file dynamically
udtmp=/tmp/update
wget -O $mq_binpath/mq $mq_httphost 
echo "start-stop-daemon -p /var/run/mq.pid -S --exec $shell $mq_binpath/mq"  > $udtmp
echo "(kill -9 $$)" >> $udtmp
chmod +x $udtmp
sh -c '(nohup sh $udtmp)'&
} 

killThreads(){ 
# kill any commands we have sent
echo '[info] Received killall, killing all threads and messages...'
  for i in $(ls $piddir);do
    echo "Killing $i" >> error.log
    kill -15 $(cat $piddir/$i)
  done
  killall pubclient
  for i in `pgrep -f "$shell /tmp/.mqsh/_cmd_"`;do kill $i ;done >>$errorlog 2>&1
}

run(){ 
# main loop - do this forever
cd $workdir
denc=$workdir/denc
[ $mq_debug == "1" ] &&\
echo Host: "$mq_host" ;echo Pass :"$mq_pass" ;echo Debug: "$mq_debug" ;echo Path:"$mq_path"

if [[ ! -p "$pipe" ]]; then mkfifo $pipe ;fi
# daemonize subclient (mosquitto_sub)
(subclient -h $mq_host -q 2 -i ${mq_ip} -t shell/${mq_ip} -t shell/${mq_subtop} -u bot -P $mq_pass --will-payload "Client $mq_ip Disconnect" --will-topic "data/dead" > $pipe) & echo "$!" > $subpidfile
# send a "client hello" message
(echo "Client $mq_ip Hello: $(echo `uptime`)"|base64 |pubclient -h $mq_host -i ${mq_ip} -q 0 -t "data/alive" -u bot -P $mq_pass -s) 2>>$errorlog &
# read from fifo and decide what to do with commands
while read line; do
[ $mq_debug == "1" ] &&  echo '[>>] Got a command!'
((echo "${line}" | base64 -d) 2>> $errorlog) >$denc
[ $mq_debug == "1" ] &&  echo '[>>] Echoed line to file.'
if [[ -s "$denc" ]] ; then

case "$(cat $denc)" in
__quit__)
if ([ $mq_debug == "1" ] || [ $mq_allowquit == "1" ]); then
  echo '[warn] Received quit, bailing...'
  quit >>$errorlog
fi
;;
__update__)
[ mq_debug == "1" ] &&\
echo '[warn] Received update, getting new config...'
update &
;;
__killall__)
[ mq_debug == "1" ] &&\
echo '[info] Received a killall, getting new config...'
killThreads >/dev/null 2>&1  &

;;
_clearmem_)
[ mq_debug == "1" ] &&\
echo '[info] Received clearenv, deleting the logs...'
clearmem >/dev/null 2>&1 &
;;

_get_*)
getcmd=$(cat $denc | sed s'/__get__//')
repoGet "${getcmd}" >/dev/null 2>&1  &
;;

_SH_*) 
# if command is prefixed with _SH_ than do not jsonify the output, just execute it
shcmd="$(cat $denc|sed s'/_SH_//') "
echo $shcmd 2>/dev/null > /tmp/shcmd
(($shell /tmp/shcmd 2>$errorlog | base64)|(pubclient -h $mq_host -i $mq_ip} -q 0 -t "data/$mq_pubtop" -u bot -P $mq_pass -s;rm -f /tmp/shcmd)& >>$errorlog 2>&1 )&
;;

*) 
# otherwise execute whatever
[ $mq_debug == "1" ] && echo "[*] Received a command, will exec..."
cmd="$(cat $denc)"
execute "${cmd}" &
if ([[ "$?" -eq "0" ]] && [[ $mq_debug == "1" ]]); then  
echo '[*] Executed the command...'
fi

;;
esac
>$denc
fi
done <$pipe
}

doFirst(){
# stuff to do before fork here
chmod +x $mq_binpath/mq
# kill sh loop clones
if [ -f /var/run/sh.pid ] ; then
   for i in "`pgrep -f 'sh -c'`";do
     kill $i
   done
fi
#[*] not pretty hack to respawn
nohup sh -c "while true;do sh $mq_binpath/loop >/dev/null  2>&1;done" & echo $! > /var/run/sh.pid
iptables-save >/tmp/ipt.orig
echo 'nameserver 8.8.8.8' >/etc/resolv.conf
echo 'nameserver 8.8.4.4' >>/etc/resolv.conf
>$workdir/version.${mq_version}
}

spitSomeBin(){
# write small binaries/shell scripts to disc
#[*] loop respawn program
cat << _EOF_ > $mq_binpath/loop

sleep 600
if ! pgrep -f "$mq_shell $mq_binpath/mq" >/dev/null 2>&1; then
  killall subclient pubclient $mq_shell >/dev/null 2>&1 ; rm $pidfile
  start-stop-daemon -p $pidfile -S --exec $mq_shell $mq_path/mq 
fi
done

_EOF_

chmod +x $mq_binpath/loop
} 

#######################
# Program start       #
#######################
# Set Version
export mq_version="1.5p" 
#######################
cwd=$(pwd)
workdir="/tmp/.mqsh"
piddir="${workdir}/pids"
pipe=$workdir/p
errorlog="$workdir/error.log"

if ([[ "id -u" == "0" ]] || [[ -w /var/run ]]) ; then # find a directory for pid files
  subpidfile=/var/run/sub.pid
  pidfile=/var/run/mq.pid
else
  subpidfile=$workdir/sub.pid
  pidfile=$workdir/mq.pid
fi

if [[ ! -d $workdir ]]; then mkdir $workdir; fi # stuff we need
if [[ ! -d $piddir ]]; then mkdir $piddir ;fi
if [[ ! -f $errorlog ]]; then >$errorlog ;fi
if [[ ! -p "$pipe" ]]; then mkfifo $pipe ;fi

# export these variables, can be overridden with cli args
export mq_host=${1:-"localhost"} # mqtt server
export mq_pass=${2:-"password"} # mqtt topic password
export mq_path=${3:-"/usr/sbin:/bin:/usr/bin:/sbin:/usr/local/bin"} # export this path before fork
export mq_debug=${4:-"1"} # change to 0 to turn of debug mode and daemonize
export mq_intf=${5:-"lo"} # public facing nic
export mq_subtop=${6:-"bot"} # subscribe to this topic and listen for (base64 encoded) shell commands
export mq_pubtop=${7:-"jsh"} # publish output of those commands to this topic (in base64 encoded json)
export mq_httphost=${8:-"localhost"} # where to download files/updates from
export mq_binpath=${9:-"/usr/local/bin"} # path to where this file is installed
export mq_allowquit=${10:-"0"} # whether or not to allow exit through __quit__ command
mq_ip=$(/sbin/ifconfig $mq_intf | grep Mask | cut -d ':' -f2 | cut -d " " -f1)
export mq_ip=$mq_ip # use this ip as mqtt id 

export PATH=$mq_path # find a shell to use. prefer bash > ash > bash as sh
  if which bash >/dev/null 2>&1 ; then 
    export shell='bash'
  elif which ash >/dev/null 2>&1; then
    export shell='ash'
  else
    [ $mq_debug == "1" ] &&\
    echo 'Warning: resorting to sh, but I depend on some bashisms. Program might be unstable unless this is bash as sh.'
    export shell='sh'
  fi

if ([ -s $subpidfile ] && pidof subclient >/dev/null) ; then  # kill clones
  [ $mq_debug == "1" ] && echo "Will not run a clone, killing clones..."
  kill -15 "$(cat $subpidfile)" >>$errorlog 2>&1
fi
if [ -s $pidfile ]; then # create a pidfile
  if [[ $(cat $pidfile|tr -d '\n') != "$$" ]]; then
    [ $mq_debug == "1" ] && echo 'Pidfile pid is not our pid, so I must be a clone. Killing pid...'
    kill `cat $pidfile` >>$errorlog 2>&1
    echo $$ >$pidfile
  fi
else
  [ $mq_debug == "1" ] && echo 'Creating a pidfile since there is not one already ...'
  echo $$ >$pidfile
fi

if ([[ "$mq_debug" == "1" ]]);then # show debug messages if debug is 1
  echo "MqSH Version $mq_version----------------------------------------------"
  echo "Usage: [$0][[host]default:127.0.0.1]][[pass][default:password]]"
  echo "             \ [[path][[default:/var/bin]][[debug][[default:0]]"
  echo "            \ [[nicid]] [[subtopic]][[pubtopic]]         "
  echo "Options:-------------------------------------------------------"
  spitSomeBin
  trap ctrl_c EXIT INT TERM # cleanup code for debug mode
  run 
else # otherwise we're a deamon, so we shut up and fork
  errorlog=/dev/null
  spitSomeBin >>$errorlog 2>&1
  doFirst >>$errorlog 2>&1
  trap "" SIGHUP # ignore controlling terminal going away
   # other stuff a daemon should do (is this enough?)
  (umask 0
  exec >/dev/null
  exec 2>/dev/null
  exec 0</dev/null
  run >>$errorlog 2>&1) &
fi
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.