#!/bin/bash
#
# open-wifi-auto-connect.sh: Auto connect to open (and online)  WIFI(s).
# This script scanns for free WIFIs and connects to the one with the best quality.
# It checks if the WIFI/access point is really online.
# If no open and online WIFI could be connected or after a disconnect it 
# restarts with a new and fast scan for WIFIs, to make the PC nearly always 
# online.
# To avoid problems with duplicate ESSIDs and hidden ESSIDs this script uses
# MACs.
# 

# ----------------------------------------------------------------------------
# "THE BEERWARE LICENSE" (Revision 44):
# Dr. Rolf Freitag (rolf dot freitag at email dot de) wrote this file.
# As long as you retain this notice you can do whatever
# the GPL (GNU Public License version 3) allows with this stuff.
# If you think this stuff is worth it, you can send me money via
# paypal, and get a contribution receipt if you wish, or if we met some day
# you can buy me a beer in return.
# ----------------------------------------------------------------------------
#
# Version 2010-07-24
#

# set -u : Stop the script when a variable isn't set (add -x for debugging)
set -u 

echo "List of availible WIFI devices (if any):"
#tail -n +3 /proc/net/wireless
iwconfig 2>/dev/null | grep "ESSID" | cut -d" " -f1

if [ $# -ne 1 ]
then
 echo "Error: Not exact one parameter (the WIFI device which you can find via iwconfig); exiting."
 exit 1
fi

# device used for wifi (usually wlan0): first argument
DEVICE="$1"

# check if the device can be found
ifconfig "$DEVICE" >/dev/null 2>/dev/null
if [ $? -ne 0 ]; then
  echo "Error: Device $DEVICE not found (ifconfig $DEVICE returned not zero); exiting."
  exit 1
fi

# Make sure the script is run as root.
#if [ $EUID -ne 0 ]; then
if [ $GROUPS -ne 0 ]; then
   echo "Error: This script must be run from group root, but your group is $GROUPS; exiting."
   exit 1
fi

########## Lockfile Part #####################

sleeptime="1"           # sleeptime for creating the lockfile
retries="10"		# default number of retries of creating the lockfile: 10, should be > locktimeout*sleeptime
locktimeout="5"         # default timeout : 5 s. The lockfile will be removed
                        # by force after locktimeout seconds have passed since the lock-
                        # file was last modified/created. Lockfile is clock skew immune.
lockdir="/var/tmp"      # directory for the lock file
# Eleminate the optional bash call with sed and get this process name from basename.
this_process="$(basename "$(ps -p $$ -o cmd= | sed 's/^[^ ]*bash //')")"
lockfile="$lockdir/.lockfile.$this_process" # (hidden) lockfile name
# remove parameters
lockfile="`echo "$lockfile" | cut -d" " -f1`"

# ascertain whether we have lockf or lockfile system apps
check ()
{
  if [ -z "$(which lockfile | grep -v '^no ')" ] ; then
    echo "$0 failed: 'lockfile' utility not found in PATH." >&2
    exit 1
  fi
}

# make lockifle
lock () 
{
  typeset -i pid=0
  # check if a lockfile is present  
  if [ -f "$lockfile" ]; then 
    # check the PID in the lockfile
    pid="$(cat "$lockfile")"
    if [ $pid -eq 0 ]; then 
      echo "Could not read a valid PID from the lockfile."
      echo "Trying to remove that lockfile"
      echo "$lockfile"
      echo "."
      rm -f "$lockfile"
    else
      if kill -0 $pid 2> /dev/null; then
        echo "The locking executable with pid $pid and lockfile  \""$lockfile"\" appears to be already running."
	# check if the process with the found UID
	if [ $(ps -p $pid -o uid=) == $UID ] ; then
	  echo "The locking process has been created from the same user $UID which is running this script; exiting."
	  exit 1
	else
          echo "The locking process has been created from the different user"
          echo $(ps -p $pid -o uid=)
	  echo "; the user (UID) of this script is $UID."
	  # If you want to (try to) kill the blocking process, uncomment the following 3 lines.
	  echo "Try to kill this locking process."
  	  kill -9 $pid
	  rm -f "$lockfile"
          echo "Done killing and lockfile deletion."
	  # Maybe in the line before the next fi you should send an email to root@localhost that a user tried (or maybe caused)
	  # a DOS attack and that the blocking process (here undocumented because already killed) was killed.
        fi
      else
        echo "The locking executable with pid $pid has completed or was killed without cleaning up its lockfile"
        echo "or the locking executable has another name than this script or it is run by an other user;"
        echo "removing that lockfile"
        echo "$lockfile"
        echo "."
        rm -f "$lockfile"
      fi
    fi
    else
      echo "No old lock file found (ok)."
  fi
  # (try to) create the lockfile; wait 
  if ! lockfile -$sleeptime -r $retries -l $locktimeout "$lockfile" 2> /dev/null; then
    echo "$0: Failed: Couldn't create lockfile in time" >&2
    exit 1
  fi
  chmod u+rw "$lockfile"
  # store the pid
  echo $$ > "$lockfile"
  chmod u-wx "$lockfile"
  # A trap to delete the lockfile when the script gets killed by SIGHUP SIGINT or SIGTERM.
  # In many cases, e. g. a kernel hangup, this does not work and the checks above are necessary.
  #trap "rm -f $lockfile; exit" SIGHUP SIGINT SIGTERM
}

# cleanup
unlock () 
{
  rm -f "$lockfile"
}

#################### "main" ##############################

# lockfile: first check, then lock
check
lock

# kill other stuff which uses the device
/etc/init.d/network-manager stop 2>/dev/null

# set the device offline for configuration
ifconfig "$DEVICE" down

# set a random MAC
ran=$(cat /proc/interrupts | md5sum)
MAC=00:0$[$RANDOM%6]:${ran:0:2}:${ran:3:2}:${ran:5:2}:${ran:7:2}
echo "init: switching MAC to $MAC"
ifconfig "$DEVICE" promisc
ifconfig "$DEVICE" hw ether $MAC

# disable  the  ESSID  checking (ESSID promiscuous), no key
iwconfig "$DEVICE" channel auto
iwconfig "$DEVICE" essid any
iwconfig "$DEVICE" key off
# set auto parameters with maximum speed of 1 Mbit/s (half duplex; slow
# down to speed up), for maximum sensitivity, maximum range and maximum 
# immunity to interference because SNR and bit rate are complementary.
iwconfig "$DEVICE" txpower auto
#iwconfig "$DEVICE" commit
# Set a trasmission power of 500 mW, which is ok e. g. in the USA and works 
# with many cards. The value set here should be (smaller than or) equal to the 
# maximum of your card.
iwconfig "$DEVICE" txpower 500mW 2>/dev/null
iwconfig "$DEVICE" rate 1M auto
iwconfig "$DEVICE" frag auto
# set client mode
iwconfig "$DEVICE" mode managed
#iwconfig "$DEVICE" commit

# bring the device up
ifconfig "$DEVICE" up

# Power on
iwconfig "$DEVICE" power on 2>/dev/null

# create temporary (hidden) working directory and change into that directory
#TMPDIR0="/tmp/.$0.$$.$RANDOM.DIR1.if.txt"
#mkdir "$TMPDIR0"
TMPDIR0=`mktemp -d -p /tmp ."$RANDOM"_XXX`
cd "$TMPDIR0"

# bash trap function for cleanup at exit (executed e. g. when CTRL-C is pressed)
# Signals: 1/HUP, 2/INT, 3/QUIT, 9/KILL, 15/TERM, ERR, EXIT
trap bashtrap 2 9 15 EXIT
bashtrap()
{
  cd -
  rm -f /var/lib/dhcp*/dhclient.leases
  rm -rf "$TMPDIR0"
  unlock
  exit 0
}

# create temporary files
#TMPFILE0="$TMPDIR0/.$0.$$.$RANDOM.0.if.txt"
#TMPFILE0=`tempfile -d "$TMPDIR0"` # tempfile is not part of the coreutils
TMPFILE0=`mktemp --tmpdir="$TMPDIR0" ."$RANDOM"_XXX`
TMPFILE1=`mktemp --tmpdir="$TMPDIR0" ."$RANDOM"_XXX`

# variables for open WIFI count (0...), etc.
typeset -i OPENCOUNT=0
typeset -i CLOSEDCOUNT=0
typeset -i CELLCOUNT=0
typeset -i i=0
typeset -i j=0
typeset -i k=0
typeset -i l=0
typeset -i m=0
typeset -i pi=0
typeset -i pj=0
declare -a APMAC
declare -a OPENCELLNUMBER
declare -a CHANNEL
declare -a ESSID
typeset -i deadline_counter=0
typeset -i loop_counter=0
typeset -i connected=0
typeset -i flag=0
typeset -i SCANNUMBER=0
typeset -i counter0=0
typeset -i counter1=0

# start of the endless loop with scanning, looking for open WIFIs, testing/using
while [ 1 == 1 ]
do
  OPENCOUNT=0

  # Change the MAC before every scan to a random and valid MAC by limiting the second byte to 5.
  # Because modern WIFI cards usually have higher bytes, this ensures another and random MAC.
  ran=$(head /dev/urandom | md5sum)
  MAC=00:0$[$RANDOM%6]:${ran:0:2}:${ran:3:2}:${ran:5:2}:${ran:7:2}
  echo "switching MAC to $MAC"
  ifconfig "$DEVICE" down
  ifconfig "$DEVICE" promisc
  ifconfig "$DEVICE" hw ether $MAC
  # clear the arp cache
  #ip neigh flush all 2>/dev/null
  ifconfig "$DEVICE" up

  # scan and get a list of (open) wifi points
  echo "Scan number $SCANNUMBER, scanning ..."
  SCANNUMBER=$[$SCANNUMBER +1]
  iwlist "$DEVICE" scanning > "$TMPFILE0" 2>/dev/null

  # remove the first line of the scan output (with "Scan completed")
  i=`wc -l < "$TMPFILE0"`
  i=$[$i -1]
  tail -n $i "$TMPFILE0" > "$TMPFILE1"
  
  # get the number of open WIFIs
  cat "$TMPFILE1" | grep "Encryption key:off" > "$TMPFILE0"
  OPENCOUNT=`wc -l < "$TMPFILE0"`

  # get the number of closed WIFIs
  cat "$TMPFILE1" | grep "Encryption key:on" > "$TMPFILE0"
  CLOSEDCOUNT=`wc -l < "$TMPFILE0"`

  echo "Found $OPENCOUNT open WIFI(s) and $CLOSEDCOUNT closed WIFI(s)."

  # get the total number of WIFIs (Cells)
  cat "$TMPFILE1" | grep "Encryption key:" > "$TMPFILE0"
  CELLCOUNT=`wc -l < "$TMPFILE0"`

  # Split the scan output into one file per cell; the files are xx1, xx2, ...
  csplit --digits=1 -k "$TMPFILE1" '/Cell/' {99} 2> /dev/null > /dev/null

  # print the WIFI data
  echo "List of WIFI(s) with Channel, Encryption, Quality, Signal Level, MAC, ESSID:"
  i=1 # number for the first open WIFI MAC
  for loop_counter in $(seq 1 $CELLCOUNT)
  do
      echo -n "`cat xx$loop_counter | awk '/Channel:/{ print $1 }'`"
      echo -n -e "\t`cat xx$loop_counter | awk '/Encryption/{ print $2 }' | cut -d":" -f2`"
      echo -n -e "\t`cat xx$loop_counter | awk '/Quality/{ print $1}'`"
      echo -n "  `cat xx$loop_counter | awk '/Quality/{ print $3}'`"
      echo -n "  `cat xx$loop_counter | awk '/Address/{ print $5 }'`"
      echo "  `cat xx$loop_counter | awk '/ESSID/{ print $1 }'`"
  done

  # if no open WIFI found: continue (make a new scan)
  if [ $OPENCOUNT -eq 0 ]
  then
    continue
  fi

  # Now we have at minimum one open WIFI. 
  # Create the list of open WIFIs (OPENCELLNUMBER[1]...OPENCELLNUMBER[$OPENCOUNT])
  i=1 # first number of the first open WIFI
  for loop_counter in $(seq 1 $CELLCOUNT)
  do
    cat xx$loop_counter | grep "Encryption key:off" 2>&1 >/dev/null
    if [ $? -eq 0 ]; then
      OPENCELLNUMBER[$i]=$loop_counter
      i=$[$i +1]
    fi
  done
  
  # Sort the list with swapsort by quality: highest/best first.
  # This takes n*(n-1)/2 comparisons (and maybe swaps), e. g. 45 for n=10, 499500 for n=1000, 
  # so with really lots of open WIFIs you should use something really faster, like GPU-Quicksort or GPUSort.
  for i in $(seq 1 $[$OPENCOUNT -1])
  do
    l=${OPENCELLNUMBER[$i]}
    qi=`cat xx$l | awk '/Quality/{ print $1}' | cut -d"=" -f2 | cut -d"/" -f1`    
    for j in $(seq $[$i +1] $OPENCOUNT)
    do
      m=${OPENCELLNUMBER[$j]}
      qj=`cat xx$m | awk '/Quality/{ print $1}' | cut -d"=" -f2 | cut -d"/" -f1`
      if [ $qi -lt $qj ]; then
        OPENCELLNUMBER[$i]=$m  # swap
        OPENCELLNUMBER[$j]=$l  # swap
      fi
    done
  done

  # Now the strongest WIFI is at OPENCELLNUMBER[1], the weakest at OPENCELLNUMBER[$OPENCOUNT]
  # put the data of the open WIFIs into arrays 
  for i in $(seq 1 $OPENCOUNT)
  do
    l=${OPENCELLNUMBER[$i]}
    APMAC[$i]=`cat xx$l | awk '/Address/{ print $5 }'`
    CHANNEL[$i]=`cat xx$l | awk '/Channel:/{ print $1 }' | cut -d ":" -f 2`
    foo=`cat xx$l | awk '/ESSID:/{ print $1 }' | cut -d ":" -f 2 | cut -c 2-128`
    ESSID[$i]=${foo%?}
  done
  # now the MACs of the open WIFIs are at APMAC[1]...APMAC[$OPENCOUNT]

  # Check/use the list of open WIFI(s)
  for loop_counter in $(seq 1 $OPENCOUNT)
  do
    deadline_counter=0
    connected=0

    echo "Checking the open WIFI with MAC ${APMAC[$loop_counter]}, Channel ${CHANNEL[$loop_counter]}, ESSID ${ESSID[$loop_counter]}"
    while [ $deadline_counter -lt 2 ]  # give a connection try at minimum two chances
    do
      if [ $connected -eq 0 ]; then # if not connected
        # kill the now outdated dhcpcd
        dhcpcd -k 2>/dev/null
        # check termination of dhcpcd
        if [ -f /var/run/dhcpcd-"$DEVICE".pid ]
        then
          kill `cat /var/run/dhcpcd-"$DEVICE".pid`
          rm -f /var/run/dhcpcd-"$DEVICE".pid
        fi
        # kill the now outdated dhclient
        if [ -f /var/run/dhclient.pid ]
        then
          #dhclient -r
          kill `cat /var/run/dhclient.pid`
          rm -f /var/run/dhclient.pid
        fi
        killall dhclient 2>&1 >/dev/null
        rm -f /var/lib/dhcp*/dhclient.leases

        # Connect
        iwconfig "$DEVICE" mode managed ap "${APMAC[$loop_counter]}" channel "${CHANNEL[$loop_counter]}" essid ${ESSID[$loop_counter]}
        # iwconfig commit

        # DHCP configuration: use dhcpcd if availible, dhclient else
	# if command -s -v dhcpcd 
        type -P dhcpcd
        if [ $? -eq 0 ]
        then # dhcpcd with 20 s timeout (default 60)
          dhcpcd -t 20 "$DEVICE"
        else
          dhclient -1 "$DEVICE"
        fi
        
        # Check if we can be connected: Because dhclient always returns 0, check if we got an ip
        ifconfig "$DEVICE" | grep "inet "
        if [ $? -ne 0 ]
        then
          echo "DHCP got no ip."
          type -P dhcpcd
          if [ $? -ne 0 ]
          then
            dhclient -r
            rm -f /var/lib/dhcp*/dhclient.leases
          fi
          deadline_counter=$[$deadline_counter +1]
          connected=0
          continue
        fi

        connected=1 # we should now be connected to the access point
      fi
      echo -n "Connectet: "
      iwconfig "$DEVICE" | grep -i "quality"

      # check the internet connection first by DNS lookups, than test downloads
      counter0=0

      # DNS lookup for DNS root nameserver D
      foo=`dig -p 53 +time=5 -4 +short +noidentify terp.umd.edu`
      if  [ "$foo" == "128.8.10.90" ]
      then
        counter0=$[$counter0 +1]  # ok counting
      fi

      # DNS lookup for dns.msftncsi.com, see http://technet.microsoft.com/en-us/library/cc766017%28WS.10%29.aspx
      foo=`dig -p 53 +time=5 -4 +short +noidentify dns.msftncsi.com`
      if  [ "$foo" == "131.107.255.255" ]
      then
        counter0=$[$counter0 +1]
      fi

      if [[ ($counter0 -ne 0)  ||  ($connected -ne 0) ]]  # at minimum one DNS lookup was successfull or we were online before; we may be online
      then
        echo "DNS lookup check: $counter0 DNS lookup(s) successfull!"
        counter1=0

        # test download section
        USER_AGENT=Mozilla/4.0\ \(compatible\;\ MSIE\ 7.0\;\ Windows\ NT\ 6.0\;\ SLCC1\;\ .NET\ CLR\ 2.0.50727\;\ .NET\ CLR\ 3.0.04506\)
        # download the small google logo
        rm -f file
        wget -np --proxy=off --timeout=5 --tries=1 --user-agent="$USER_AGENT" --header="Accept-Encoding: deflate, gzip" --output-document=file http://www.google.com/images/logo_sm.gif 2>/dev/null
        # check the download 
        echo "46d45b3b923c33542f82722f6bfdda53bd485be5  file" > file.sha1
        sha1sum -c file.sha1 2>/dev/null
        counter1=$[$counter1 +$?] # error counting

        # download the SM test file
        rm -f file
        wget -np --proxy=off --timeout=5 --tries=1 --user-agent="$USER_AGENT" --header="Accept-Encoding: deflate, gzip" --output-document=file http://www.msftncsi.com/ncsi.txt 2>/dev/null
        # check the download 
        echo "33bf88d5b82df3723d5863c7d23445e345828904  file" > file.sha1
        sha1sum -c file.sha1 2>/dev/null
        counter1=$[$counter1 +$?]

        if [ $counter1 -ge 2 ]  # all test downloads failed
        then
          echo "all test downloads failed"
          # increase the deadline counter if we were not connected before or
          # if we were connected but also all DNS lookups failed
          if [ $connected -eq 0 ]
          then
            deadline_counter=$[$deadline_counter +1]
          else
            if [ $counter0 -eq 0 ]
            then
              deadline_counter=$[$deadline_counter +1]
            else # at minimum one dns lookup was successfull; we are half online
              sleep 1 # wait a second before the next checks
            fi
          fi
        else
          # we are online; wait before the next check
          echo "Online!"
          sleep 10 
        fi
      else  # offline; no DNS lookup was successfull and we were not online before
        deadline_counter=$[$deadline_counter +1]
        echo "offline"
      fi
    done

    # the actual open WIFI failed the online test two times; loop to the next
    deadline_counter=0
    connected=0
    echo "Not connectet"
  done # go to the next open WIFI (or make a new scan)

done # while [ 1 = 1 ],  end of the endless loop

exit 0