Openwrt busybox v1.36.1 doesn't have command last?

I've found that OpenWrt 24.10.1 doesn't have last command. Thus, I had to write a simple last command. However, how do I keep track of luci user login ? Any suggestion ?

last.sh

#!/bin/ash
bname=$(basename $0)
VERSION="$bname from $(uname) $(uname -r) Ver.0.1a" #Jun 9, 2025

succ=""
succTstmp=""
succPid=""
succNm=""
succIP=""
escSymbol="&"

disp="" #Output result

par=""
isShowTS=0 #0 = false show readable date time, 1 = true show timestamp
isShowTitle=0 #0 = false not show header, 1 = true show header
columnname=""

#Coloring Result
#=====================================================================================

#Header 
bc='\033[1;36m'
et='\033[m'

husr="$bc""User""$et";hremote="$bc""Remote IP""$et";hloginT="$bc""Login Time""$et";hdur="$bc""Duration""$et";hstat="$bc""Status""$et"

#Up and Down Status
ubc='\033[1;32m'
dbc='\033[1;31m'

uPstat="$ubc""Up""$et";dNstat="$dbc""Down""$et"

#=====================================================================================

#Append variable to 'par' variable
while [ -n "$1" ]; do
	par="$par$1 " 
	shift
done

#generate column header
function showtitle () {
	columnname=""

	
	if [ $isShowTitle -eq 1 ]
	then
		if [ $isShowTS -eq 0 ]
		then
			# format for readable date time header
			columnname="$husr"$'\t'"$hremote"$'\t'$'\t'"$hloginT"$'\t'$'\t'"$hdur"$'\t'"$hstat"$'\n'	
		else
			# format for timestamp header
			columnname="$husr"$'\t'"$hremote"$'\t'$'\t'"$hloginT"$'\t'"$hdur"$'\t'"$hstat"$'\n'	
		fi
		columnname="$columnname=============================================================================="$'\n'
	fi
}


function helpdoc () {
	echo $'\n'"Usage: $bname [-t][-T][?][--help]"$'\n'
	echo "Show a listing of last logged in users."$'\n'
	echo "Options:"
	echo " -t, --title"$'\t'$'\t'"show column title"
	echo " -T, --timestamp"$'\t'"show timestamp instead of human readable date and time"
	echo " -h, --help"$'\t'$'\t'"display this help"
	echo ""		
}

function parameterhandler () {
	for itm in $par;do
		case $itm in
			#Inject Title to output
			-t) isShowTitle=1
                        ;;
                        #Show Timestamp, instead of human readable date
			-T) isShowTS=1 
			;;
			#Print Usage
			\?|--help) helpdoc; exit
			;;			
			#Print Usage
			-V|--version) echo $VERSION; exit			
			;;
			esac
	done
}


#Connect record Structure:	TimeStamp	PID	Usrname		IP
conrec=""
function retriveSucc () {
	submatch1="[[:alpha:]]{3}[[:blank:]]*[[:alpha:]]{3}[[:blank:]]*[[:digit:]]+[[:blank:]]*[[:digit:]]{2}:[[:digit:]]{2}:[[:digit:]]{2}[[:blank:]]*[[:digit:]]{4}[[:blank:]]*\[([[:digit:]]+\.[[[:digit:]]{3})\]"

	#Match PID: dropbear[7966]
	submatch2="dropbear\[([[:digit:]]+)\]"


	#Match login name: Password auth succeeded for 'root'
	submatch3="Password[[:blank:]]*auth[[:blank:]]*succeeded[[:blank:]]*for[[:blank:]]*'([^\']+)'"
	
	#Match ip name: from 10.10.1.55:51128$:
	submatch4="from[[:blank:]]*([^$]+)"

	tmp1=""
	tmp1=$(printf '%s' "$succ" | sed -nE "s/^$submatch1.*$submatch2.*$submatch3.*$submatch4$/\1 \2 \3 \4/p" | tr '\n' '\n')

	conrec=$(printf "%s" "$tmp1" | tr '\n' "$escSymbol" | sed -E "s/[$escSymbol]*$//g" | tr "$escSymbol" '\n' | sort) #Replace the end \n symbol
}


#disconnect record Structure:	TimeStamp	PID	Usrname		IP
disrec=""
function retrieveDisc () {
	#Match timestamp: Wed Jun  4 02:16:11 2025 [1749003371.761]
	submatch1="[[:alpha:]]{3}[[:blank:]]*[[:alpha:]]{3}[[:blank:]]*[[:digit:]]+[[:blank:]]*[[:digit:]]{2}:[[:digit:]]{2}:[[:digit:]]{2}[[:blank:]]*[[:digit:]]{4}[[:blank:]]*\[([[:digit:]]+\.[[[:digit:]]{3})\]"

	#Match PID: dropbear[7966]
	submatch2="dropbear\[([[:digit:]]+)\]"

	#Match login name: Exit (root) or  (user 'root', 0 fails)
	submatch3="(\(([[:alnum:]]+)\)|'([[:alnum:]]+)')"

	#Match ip name: from <10.10.1.55:51128>:
	submatch4="from[[:blank:]]*<([^>]+)>[[:blank:]]*:"
	
	tmp1=$(printf '%s' "$disc" | sed -nE "s/^$submatch1.*$submatch2.*$submatch3.*$/\1 \2 \4\5/p")
	tmp2=$(printf '%s' "$disc" | sed -nE "s/^.*$submatch4.*$/\1/p")

	IFSold=$IFS
	#line separator for variable
	IFS=$'\n'""

	k=0

	disrec="" #Make use it is empty	
	for item1 in $tmp1
	do
		disrec="$disrec$item1"
		j=0
		for item2 in $tmp2
		do
			if [ $k -eq $j ] 
			then
				disrec="$disrec $item2"$'\n'"" #Append IP
				break	
			fi
			j=$(($j+1))
		done	
		k=$(($k+1))
	done
	
	disrec=$(printf "%s" "$disrec" | tr '\n' "$escSymbol" | sed -E "s/[$escSymbol]*$//g" | tr "$escSymbol" '\n' | sort)
		 
	IFS=$IFSold
}

#exit record Structure:	TimeStamp	PID	Usrname		IP
exitrec=""
function retrieveExit () {
	# Match timestamp: Wed Jun  4 02:16:11 2025 [1749003371.761]
	submatch1="[[:alpha:]]{3}[[:blank:]]*[[:alpha:]]{3}[[:blank:]]*[[:digit:]]+[[:blank:]]*[[:digit:]]{2}:[[:digit:]]{2}:[[:digit:]]{2}[[:blank:]]*[[:digit:]]{4}[[:blank:]]*\[([[:digit:]]+\.[[[:digit:]]{3})\]"

	#Match PID: dropbear[7966]
	submatch2="dropbear\[([[:digit:]]+)\]"

	#Match login name: (user 'root',
	submatch3="\(user[[:blank:]]*'([^']+)',"

	#Match ip name: from <10.10.1.55:51128>:
	submatch4="from[[:blank:]]*<([^>]+)>[[:blank:]]*:"

	tmp1=""
	tmp1=$(printf '%s' "$ext" | sed -nE "s/^$submatch1.*$submatch2.*$submatch4.*$submatch3.*$/\1 \2 \4 \3/p")	
	
	#Take away 0 fail results, take 
	#Tue Jun  3 05:32:29 2025 [1748928749.149] authpriv.info dropbear[3556]: Exit before auth from <10.10.1.55:33686>: (user 'root', 0 fails): Exited normally
	unMatch=$(printf '%s' "$ext" | grep -vE "\(user[[:blank:]]*\'[^\']+\',[[:blank:]]*0[[:blank:]]*fails") 	
	#printf "%s" "$unMatch"
	
	#===========================================
	#UnMatch type 1: Tue Jun  3 06:54:07 2025 [1748933647.855] authpriv.info dropbear[5226]: Exit (root) from <10.10.1.224:49940>: Exited normally
	#===========================================
	unmat1=""
	unmat1=$(printf '%s' "$unMatch" | grep -E ":[[:blank:]]*Exit[[:blank:]]*\([^\)]*\)[[:blank:]]*from") #match line with
	
	#Match login name: Exit (root) from
	submatch3=":[[:blank:]]*Exit[[:blank:]]*\(([^\)]+)\)[[:blank:]]*"
	
	#Match ip name: from <10.10.1.55:51128>:
	submatch4="from[[:blank:]]*<([^>]+)>"	
	
	tmp2=""
	tmp2=$(printf '%s' "$unmat1" | sed -nE "s/^$submatch1.*$submatch2.*$submatch3.*$submatch4.*$/\1 \2 \3 \4/p")
	
	#===========================================
	#UnMatch type 2: Tue Jun  3 05:34:04 2025 [1748928844.274] authpriv.info dropbear[3557]: Exit before auth from <10.10.1.55:58844>: Exited normally
	#No User name specify
	#===========================================
	unmat2=""
	unmat2=$(printf '%s' "$unMatch" | grep -vE ":[[:blank:]]*Exit[[:blank:]]*\([^\)]*\)[[:blank:]]*from") #match reverse
	
	tmp3=""
	tmp3=$(printf '%s' "$unmat2" | sed -nE "s/^$submatch1.*$submatch2.*$submatch4.*$/\1 \2 Anonymous \3/p")
	
	exitrec="$tmp1"$'\n'"$tmp2"$'\n'"$tmp3"$'\n'""
	exitrec=$(printf "%s" "$exitrec" | tr '\n' "$escSymbol" | sed -E "s/[$escSymbol]*$//g" | tr "$escSymbol" '\n' | sort) #Replace the end \n symbol	)

	return 0
}

onprocrec=""
function UserOnProc () {
	#lgUsr=$(ps | grep dropbear | grep -v "`cat /var/run/dropbear.1.pid`\|grep" | awk '{print $1 }')
	lgUsr=$(ps | grep dropbear | grep -v "`cat /var/run/dropbear.1.pid`\|grep")
	currentTS="$(date +%s)"
}

#ps | grep dropbear | grep -v "`cat /var/run/dropbear.1.pid`\|grep" | awk '{print $1 }' > /tmp/usrlog
lgUsr=$(ps | grep dropbear | grep -v "`cat /var/run/dropbear.1.pid`\|grep" | awk '{print $1 }')

drobearLog=$(logread -t -e 'dropbear')

succ=$(printf '%s' "$drobearLog" | grep "Password auth succeeded for")
disc=$(printf '%s' "$drobearLog" | grep "Disconnect received")
ext="$(printf '%s' "$drobearLog" | grep 'Exited')"

function dispformat () {
	item1=$2
	test=$3
	usrIP=""
	lginTS=""
	lgoutTS=""
	duration=""
	
	usrIP=$(printf "%s" "$item1" | awk '{print $4}')
	usrIP='\033[1;33m'"$usrIP"'\033[m'
	lginTS=$(printf "%s" "$item1" | awk '{print $1}')	
	lgoutTS=$(printf "%s" "$test" | awk '{print $1}')
	duration=$(awk "BEGIN {print $lgoutTS - $lginTS}")
				
	lginTS=$(printf "%.f" "$lginTS")  #Remove decimal
	lgoutTS=$(printf "%.f" "$lgoutTS") #Remove decimal
	duration=$(printf "%.f" "$duration")  #Remove decimal
	
	if [ $isShowTS -eq 0 ] #not show timestamp
	then
		duration="$(printf '%dh:%dm:%ds\n' $((duration/3600)) $((duration%3600/60)) $((duration%60)))"
		lginTS=$(date +'%y-%m-%d %H:%M:%S' -d @"$lginTS")
		disp="$disp""$usrNm"$'\t'"$usrIP"$'\t'"$lginTS"$'\t'"$duration"$'\t'"$1"$'\n' 
	else
		disp="$disp""$usrNm"$'\t'"$usrIP"$'\t'"$lginTS"$'\t'"$duration"$'\t'$'\t'"$1"$'\n' #header format for showing timestamp 
	fi
}

function checkStatus () { 
	IFSold=$IFS
	#line separator for variable
	IFS=$'\n'""
	test1=""
	test2=""
	test3=""

	k=0

	showtitle #check if show header
	ubColor="\033[1;32m" # color Up begin
	dbColor="\033[1;31m" # color Down begin 	
	deColor="\033[m"     # color end
	
	for item1 in $conrec
	do
		#echo "$k)$item1"
		usrPID=$(printf "%s" "$item1" | awk '{print $2}')
		usrNm=$(printf "%s" "$item1" | awk '{print $3}')
		#echo $usrPID"  "$usrNm
		test1=""
		test1=$(printf '%s' "$disrec" | grep -E ".*$usrPID.*$usrNm")
		
		if [ -n "$test1" ]
		then
			dispformat "$dNstat" "$item1" "$test1"		
		else
			test2=$(printf '%s' "$exitrec" | grep -E ".*$usrPID.*$usrNm") #If exited

			if [ -n "$test2" ]
			then
				dispformat "$dNstat" "$item1" "$test2"
			else
				currentTS="$(date +%s)"			
				test3=$item1
				test3=$(printf "%s" "$test3" | awk '$1='"$currentTS")
				dispformat "$uPstat" "$item1" "$test3"
			fi
		fi
		
		k=$(($k+1))
	done
	
	IFSold=$IFS
	
	return 0 
}
parameterhandler
retriveSucc #Extract login 
retrieveDisc #Extract logout 
retrieveExit #Extract Exit
checkStatus

printf "$columnname""$disp" #Print output

Nice work

Alternatively compile your own build and add the last command to busybox config :slight_smile:

3 Likes