#!/usr/bin/python
#
# Livetest
# Tests for functionality of basic ipsec features.
#
# Copyright (C) 2009 Daniel Snider <danielsnider12@gmail.com>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
#
# 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.

import os
import sys
import commands
import cgitb
import httplib
import urllib
import subprocess
import datetime
import time
import signal
import datetime
import getopt

natflag = 0
retryflag = 0
nocolour = 0
returnhelp = 0
verbose = 0

ADDRWEB = "206.248.173.87" #livetest server
ADDRSEC = "206.248.173.86" #livetest libreswan server
configsetup = "config setup"
nat_traversal = "nat_traversal=yes"

def outmsg(msg):

	sys.stdout.write(msg)
	sys.stdout.flush()
	return len(msg)

#function to print output
def output(mod, state):

	if nocolour:
		s = "\t[" + state + "]\n"
		sys.stdout.write (s.expandtabs(70 - mod - len (state)))
		if state == "FAIL": sys.exit(1)

	else:
		s = "\t["
		sys.stdout.write(s.expandtabs(70-mod-len(state)))

		if (state == "OK") | (state == "YES") | (state == "NO"): sys.stdout.write ('\033[92m')
		elif (state == "FAIL") | (state == "FAILED"): sys.stdout.write ('\033[91m')
		else: sys.stdout.write ('\033[93m')

		sys.stdout.write (state)
		sys.stdout.write ('\033[0m')
		sys.stdout.write ("]\n")

		if state == "FAIL": sys.exit(1)

#runs commands and stops after timeout seconds
def timeout_command(command, timeout):
	
	  #call shell-command and either return its output or kill it
          #if it doesn't normally exit within timeout seconds and return None
	  cmd = command.split(" ")
          start = datetime.datetime.now()
          process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

	  while process.poll() is None:
              time.sleep(0.2)
              now = datetime.datetime.now()
              if (now - start).seconds> timeout:
                  os.kill(process.pid, signal.SIGKILL)
                  os.waitpid(-1, os.WNOHANG)
                  return None
	
	  return process.wait()

#Check pluto. It should not be running
def checkPluto():

	(status,output) = commands.getstatusoutput("ipsec setup status")
	
	#return values:	
	#0 - ipsec running	
	#256 - could be started or stopped
	#512 - subsystem locked
	#768 - ipsec stopped
	
	if "can not load config" in output:
		if nocolour: print "\nError: " + output
		else: print "\n\033[91mError\033[0m: " + output
		sys.exit(1)	
	
	elif status == 0:
		if returnhelp: print "\n\nTo stop pluto use the command: \"ipsec setup stop\"\n"	
		return "FAIL"
	
	elif status == 256:
		outp = commands.getoutput("ipsec ikeping")
		if "v4 bind: Address already in use" in outp:
			if returnhelp: print "\n\nTo stop pluto use the command: \"ipsec setup stop\"\n"	
			return "FAIL"	
		else:
			return "OK"

	elif status == 512: return "LOCKED"	
	elif status == 768: return "OK"
	else: return "UNKNOWN"

#Check for installed libreswan
def checkInstall():
	
	(status,output) = commands.getstatusoutput("ipsec --version")
	
	if status == 0: return "OK"
	
	else: return "FAIL"

#check for SElinux enforce
def checkSElinux():

	if os.path.exists("/selinux/enforce"): return "FAILED"

	else: return "OK"
	
#check for ipforwarding
def checkForwarding():

	file = open("/proc/sys/net/ipv4/ip_forward", 'r')
	contents = file.read() 	
	
	if "0" in contents:
		if returnhelp: print "\n\nTry \"echo \"1\" > /proc/sys/net/ipv4/ip_forward\" to fix this problem\n"
		return "WARNING"
		
	elif "1" in contents: return "OK"
	else: return "FAIL"

#check that send and accept icmp redirects are disabled if netkey is detected
def check_icmp_redirect(option):

	if "1" in open("/proc/sys/net/ipv4/conf/default/" + option + "_redirects").read():
		if returnhelp: print "\n\nTry \"for f in /proc/sys/net/ipv4/conf/*/"+option+"_redirects; do echo 0 > $f; done\"	to fix this problem\n"
		return "WARNING"
	else: return "OK"

#check for IPsec suppor in kernel
def checkKern():

	(status,output) = commands.getstatusoutput("ipsec --version")
	
	if "no kernel code" in output:
		if (commands.getstatusoutput("modprobe af_key")[0] != 0) & (commands.getstatusoutput("modprobe ipsec")[0] != 0): return "FAIL"
		else: return "OK"

	else: return "OK"

#Check that ip command is present
def checkCommandIP():

	(status,output) = commands.getstatusoutput("which ip")
	
	if status == 0: return "OK"
        else: return "FAIL"	

#Check that ip command is present
def checkCommandIPtables():

        (status,output) = commands.getstatusoutput("which iptables")

        if status == 0: return "OK"
        else: return "FAIL"

#Check for internet connectivity
def checkNet():

	(status,output) = commands.getstatusoutput("ping " + ADDRWEB + " -c 1")

	if status == 0: return "OK"
	else: return "FAIL"
	
def getAppearentIP():

	conn = httplib.HTTPConnection(ADDRWEB, 80)
	conn.request("GET", "/cgi-bin/returnip.py")
	r1 = conn.getresponse()
	appearent =  r1.read()
	conn.close()

	return appearent.strip("\n")

#definitively get local ip from default route
def getLocalIP():

	interfaces = commands.getstatusoutput("ip ro list")

	split_if = interfaces[1].split("\n")

	for line in split_if:
		if "default" in line:
			default = line.split(" ")[4]

	ifconfig = commands.getstatusoutput("ifconfig")
	split_ifconfig = ifconfig[1].split("\n\n")

	for line in split_ifconfig:
		if default in line:
			return (line.split(":")[7]).split(" ")[0]


#Compare local IP to percieved remote IP
def checkNAT():	

	global natflag	
	
	#compare local ips to the appearent one	
	if getLocalIP().find(getAppearentIP()) == -1:
		natflag = 1 #you are behind natt	
		return "YES"	

	else:
		natflag = 0 #you are not behind natt	
		return "NO"

#check to make sure NAT address is within RFC1918 address space
def checkLocalIP():

	local = getLocalIP()

	if "192.168." not in local:
		if "172.10" not in local:
			if "10." not in local: return "FAILED"
	return "OK"

#open ipsec.conf
def openConf():

	(status, dir) = commands.getstatusoutput("ipsec --confdir")

	f = open("/etc/ipsec.conf", 'r')

	if f: return "OK"
	else: return "FAIL"

#check ipsec.conf
def checkConf(s):

	f = open("/etc/ipsec.conf", 'r')

	for line in f:
		if ("#" + s) in line: return "FAIL"
		elif s in line: return "OK"

	return "FAIL"

#port check host originating
def checkPort(port, addr):
	
	global retryflag, verbose
	cmd = "ipsec ikeping --ikeport " + str(port) + " " + addr + "/" + str(port)
	(status,output) = commands.getstatusoutput("ipsec ikeping --ikeport " + str(port) + " " + addr + "/" + str(port))

	if verbose:
		print "\nPrinting command: " + cmd
		print "Printing output: " + output + "\n"
	
	if (status == 0) | (status > 60000):
		retryflag = 0	
		return "OK"

	else:
		retryflag = 1	
		return "FAILED"

#start libreswan
def startPluto():

	status = timeout_command("ipsec setup start", 10)

	if status == 0: return "OK"

	elif "None" in str(status): return "LAG"

	else: return "FAIL"

#try to set up tunnel
def whack(command, timeout):

	global retryflag	
	retryflag = 0
	
	if timeout == 0:
		(status,output) = commands.getstatusoutput(command)

		if status == 0: return "OK"
		else: return "FAILED"
	
	else:
		status = timeout_command(command, timeout)

		if status == 0: return "OK"

              	elif "None" in str(status):
			retryflag = 1	
			return "FAILED" #more logic could be here for why a command stalled

		else:
			retryflag = 1	
			return "FAILED"

#ping threw tunnel
def livetestPing():

	global retryflag
        retryflag = 0

	#commands.getstatusoutput("ping " + ADDRSEC + " -c 1")
	(status,output) = commands.getstatusoutput("ping " + ADDRSEC + " -c 1")

	if status == 0: return "OK"

        else:
		retryflag = 1	
		return "FAILED"

#check secure log for tunnel errors
def checkLog():

	#this is currently operating system dependent

	global retryflag	
	retryflag = 0

	log_path = "/var/log/auth.log"

	(status,output) = commands.getstatusoutput("tail " + log_path)

	if "differs from size specified in ISAKMP HDR" in output:
		return "expected packet size differs"

	elif "malformed payload notifies" in output:
		return "possible key error" 	

	elif "INVALID_KEY_INFORMATION" in output:
                return "key error"

	elif "INVALID_ID_INFORMATION" in output:
		return "ID error"

	elif "unknown exchange" in output:
                return "unknown exchange"

	elif "Quick Mode message is for a non-existent" in output:
		return "pfs not supported?"

	else: return "not-sure"

def checkMTU(timeout):
	#Exception to raise on a timeout
        class TimeExceededError(Exception):
                pass

	#connection execution
	def mtu():
		conn = httplib.HTTPConnection(ADDRSEC, 80)
		conn.request("GET", "/icons/f.png")
		r1 = conn.getresponse()
		appearent =  r1.read(10)
		conn.close()

	#an alarm singal handler
	def alarmHandler(signum, frame):
		raise TimeExceededError, "Your command ran too long"

	#set the alarm signal handler, and set the alarm to 'timeout' seconds
	signal.signal(signal.SIGALRM, alarmHandler)
	signal.alarm(timeout)

	try:
		mtu()
		signal.alarm(0)
		return "OK"	
	except TimeExceededError:
		return "FAILED"

def stopPluto():

	(stats,outpt) = commands.getstatusoutput("ipsec auto --delete livetest")
	(status,output) = commands.getstatusoutput("ipsec setup stop")

	if status == 0: return "OK"
	else: return "FAILED"

#barfanalyse
def barfAnalyze():

        conn = httplib.HTTPConnection(ADDRWEB, 80)
        conn.request("GET", "/cgi-bin/barfanalyze.py")
        r = conn.getresponse()

        sys.stdout.write(r.read())
        sys.stdout.flush()

        conn.close()

#barfupload
def barfUpload():

	answer = raw_input("Would you like to additional checks done? It requires your ipsec barf info to be sent to Xelerance. But it's instant. Y/N: ")

	if answer in ["Y", "y"]:

		barf = commands.getoutput("ipsec barf")
		file_path = "/home/daniel/livetest/client/old/barf.txt"

		data = urllib.urlencode({"file" : barf})
		f = urllib.urlopen("http://livetest.libreswan.org/cgi-bin/barfload.py", data)

		barfAnalyze()

		print "To view dubugging information on your livetest tunnel as well as your ipsec barf go to /http://livetest.libreswan.org/cgi-bin/debug_info.py"


def main():
	
	ADDRWEB = "206.248.173.87" #livetest server
	ADDRSEC = "206.248.173.86" #livetest libreswan server
	no_net = 0
	quick = 0
	configsetup = "config setup"
	nat_traversal = "nat_traversal=yes"
	global retryflag

	#check privileges
	if os.getuid() != 0:
		sys.exit("You are not superuser")

	try:
		opts, args = getopt.getopt(sys.argv[1:], 'nvrbiq' ,['nocolour','verbose','returnhelp','barfupload','nointernets','quick'])
		for o, a in opts:
			if (o == "--nocolour") | (o == "-n"):
				global nocolour	
				nocolour = 1
			if (o == "--returnhelp") | (o == "-r"):
				global returnhelp
				returnhelp = 1	
			if (o == "--verbose") | (o == "-v"):
				global verbose	
				verbose = 1
			if (o == "--barfupload") | (o == "-b"):
				barfUpload()
				sys.exit(0)
			if (o == "--nointernets") | (o == "-i"):
				no_net = 1	
			if (o == "--quick") | (o == "-q"):
				quick = 1

	except getopt.GetoptError:
		print "usage:"
		print "  -v, --verbose \tprints more connection info".expandtabs(25)
		print "  -r, --returnhelp \tprints some suggestions to help fix warnings or failures".expandtabs(25)
		print "  -n, --nocolour \tprints output without colour".expandtabs(25)
		print "  -b, --barfupload \tuploads and analyzes your ipsec barf".expandtabs(25)
		print "  -i, --nointernets \twill run commands that don't require internet".expandtabs(25)
		print "  -q, --quick \tomits port tests".expandtabs(25)
		print "  -h, --help \tprints this help".expandtabs(25)
		sys.exit(2)

	#check for network connectivity
	if not no_net:
		interfaces = commands.getstatusoutput("ip ro list")
		if interfaces[1] == "":
			sys.exit("You do not have a network connection. Try ipsec livetest -i")


	try:
		print "Checking your system to see if IPsec will work:"	
		output (outmsg("Checking that Libreswan is installed"), checkInstall())
		output (outmsg("Pluto must be stopped for this program"), checkPluto())
		output (outmsg("Checking for SElinux"), checkSElinux())
		output (outmsg("Check IP forwarding"), checkForwarding())
		
		if os.path.exists("/proc/net/pfkey"):	
			output (outmsg("NETKEY detected, testing for disabled ICMP send_redirects"), check_icmp_redirect("send"))
			output (outmsg("NETKEY detected, testing for disabled ICMP accept_redirects"), check_icmp_redirect("accept"))

		output (outmsg("Checking for ipsec support in kernel"), checkKern())
		output (outmsg("Checking for 'ip' command"), checkCommandIP())
		output (outmsg("Checking for 'iptables' command"), checkCommandIPtables())
		try:
			if not no_net: output (outmsg("Checking Internet connectivity"), checkNet())
		except SystemExit:
			try: output (outmsg("Retry: Checking Internet connectivity"), checkNet())
			except SystemExit:
				if commands.getstatusoutput("ping www.google.com -c 1")[0] == 0:
					print "Livetest's test facility is currenlty offline. Sorry for the inconvienence, please try again later."
					sys.exit(1)
				else: sys.exit(1)

		if not no_net: getAppearentIP()
		if not no_net: output (outmsg("Are you behind NAT?"), checkNAT())
		if natflag: output (outmsg ("Checking your local IP"), checkLocalIP())
		output (outmsg("Opening ipsec.conf"), openConf())
		output (outmsg("Searching /etc/ipsec.conf for \"" + configsetup + "\""), checkConf(configsetup))
		if natflag: output(outmsg ("Searching /etc/ipsec.conf for \"" + nat_traversal + "\""), checkConf(nat_traversal))
		if not no_net and not quick: output (outmsg("Testing port 500"), checkPort(500,ADDRSEC))
		if retryflag: output (outmsg("Retry: Checking port 500"), checkPort(500,ADDRSEC))
		if not no_net and not quick: output (outmsg("Testing port 4500"), checkPort(4500, ADDRWEB))
		if retryflag: output (outmsg("Retry: Checking port 4500"), checkPort(4500, ADDRWEB))
		if not no_net and not quick: output (outmsg("Testing port 5000"), checkPort(5000, ADDRSEC))
		if retryflag: output (outmsg("Retry: Checking port 5000"), checkPort(5000, ADDRSEC))
		if not no_net: output (outmsg("Starting Libreswan"), startPluto())
		
		if natflag:
			conn = "ipsec whack --name livetest --id @client --host " + getLocalIP() + " --forceencaps --to --id @lab --host 206.248.173.86 --tunnel --encrypt --psk --pfs"	
			output (outmsg("Adding \"livetest-nat\" connection"), whack(conn, 0))
			if verbose: print conn

		elif not no_net:
			conn = "ipsec whack --name livetest --id @client --host " + getLocalIP() + " --to --id @lab --host 206.248.173.86 --tunnel --encrypt --psk --pfs"
			output (outmsg("Adding \"livetest\" connection"), whack(conn, 0))
			if verbose: print conn
		
		if not no_net: output (outmsg("Loading \"livetest\" preshared key"), whack("ipsec whack --listen", 0))
		if not no_net: output (outmsg("Starting \"livetest\" connection (this may take sometime)"), whack("ipsec whack --initiate --name livetest", 60))
		if retryflag:
			output (outmsg("Checking log about tunnel"), checkLog())
			commands.getoutput("ipsec setup stop")	
			sys.exit(1)
		if not no_net: output (outmsg("Pinging threw connection"), livetestPing())
		if not no_net and retryflag: output (outmsg("Retry: Pinging threw connection"), livetestPing())
		try:	
			if not no_net: output (outmsg("Running MTU check"), checkMTU(10))
		except TimeExceededError:
                        pass
		if not no_net: output (outmsg("Stopping Libreswan"), stopPluto())
		if not no_net: barfUpload()

	except KeyboardInterrupt:
		commands.getoutput("ipsec setup stop")
		print
		sys.exit(1)

main()
