# MIT License

# Copyright (c) 2017 Balazs Bucsay

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import sys

if "Stateless_module.py" in sys.argv[0]:
	print("[-] Instead of poking around just try: python xfltreat.py --help")
	sys.exit(-1)

import socket
import time
import select
import os
import struct
import threading

#local files
from Generic_module import Generic_module
from interface import Interface
import controlchannel
import client
import common
import encryption
import threading
import checks

class Stateless_module(Generic_module):
	def __init__(self):
		super(Stateless_module, self).__init__()
		self.checks = checks.Checks()
		self.timeout = 1.0
		self.controlchannel = controlchannel.ControlChannel()

		# control message handlers
		self.cmh_struct  = {
			# num : [string to look for, function, server(1) or client(0), return on success, return on failure]
			# return value meanings: True  - module continues
			#						 False - module thread terminates
			# in case of Stateless modules, the whole module terminates if the return value is False
			0  : [common.CONTROL_CHECK, 		self.controlchannel.cmh_check_query, 1, True, True],
			1  : [common.CONTROL_CHECK_RESULT, 	self.controlchannel.cmh_check_check, 0, True, False],
			2  : [common.CONTROL_INIT, 			self.controlchannel.cmh_init, 1, True, True],
			3  : [common.CONTROL_INIT_DONE, 	self.controlchannel.cmh_init_done, 0, True, False],
			4  : [common.CONTROL_LOGOFF, 		self.controlchannel.cmh_logoff, 1, True, False],
			5  : [common.CONTROL_DUMMY_PACKET, 	self.controlchannel.cmh_dummy_packet, 1, True, True],
		}

		# reading/writing packets can be different based on the OS
		self.packet_writer = self.packet_writer_default
		self.packet_reader = self.packet_reader_default
		# different communication function for Unix and Windows
		self.communication = self.communication_unix

		# setting up for Windows
		if self.os_type == common.OS_WINDOWS:
			self.packet_writer = self.packet_writer_win
			self.communication = self.communication_win
			self.packet_reader = None

		# setting up for MacOS(X)
		if self.os_type == common.OS_MACOSX:
			self.packet_writer = self.packet_writer_mac
			self.packet_reader = self.packet_reader_mac

		return

	# hacky solution to decide from non-transport modules whether this module
	# is stateless or stateful
	def is_caller_stateless(self):
		return 1

	# is the new cmh list already in the control message handler structure?
	def is_in_cmh_already(self, _list):
		if len(_list) and len(self.cmh_struct):
			found = False
			for l in self.cmh_struct:
				if self.cmh_struct[l] == _list[0]:
					found = True

		return found

	# merging control message handlers into the Stateless' cmh list
	def merge_cmh(self, _list):
		if self.is_in_cmh_already(_list):
			return
		size = len(self.cmh_struct)
		for entry in _list:
			self.cmh_struct[size+entry] = _list[entry]

	# This function writes the packet to the tunnel.
	# Windows version of the packet writer
	def packet_writer_win(self, packet):
		import pywintypes
		import win32file

		overlapped_write = pywintypes.OVERLAPPED()
		win32file.WriteFile(self.tunnel, packet, overlapped_write)
		return

	# on MacOS(X) utun, all packets needs to be prefixed with 4 specific bytes
	def packet_writer_mac(self, packet):
		packet = "\x00\x00\x00\x02"+packet
		os.write(self.tunnel, packet)

	# default packet writer for Linux
	def packet_writer_default(self, packet):
		os.write(self.tunnel, packet)

	# This function reades the packet from the tunnel.
	# on MacOS(X) utun, all packets needs to be prefixed with 4 specific bytes
	# this will take off the prefix if that is needed
	# first_read True: discard the first 4 bytes / utun related
	# serverorclient 1: server, there is no 4byte prefix, it comes fro the PS
	#				 0: client, it comes from the tunnel iface directly
	def packet_reader_mac(self, tunnel, first_read, serverorclient):
		packet = os.read(tunnel, 4096)
		if first_read and not serverorclient:
			packet = packet[4:]
		return packet

	# for Linux and other unices
	# Windows is handled from the communication() unfortunately
	def packet_reader_default(self, tunnel, first_read, serverorclient):
		packet = os.read(tunnel, 4096)
		return packet

	# function to transform packets back and forth.
	# encryption, encodings anything that should be done on the packet and 
	# should be easily variable based on the config
	def transform(self, details, packet, encrypt):
		if details.get_encrypted():
			if encrypt:
				return details.get_module().encrypt(details.get_shared_key(), packet)
			else:
				return details.get_module().decrypt(details.get_shared_key(), packet)
		else:
			return packet

	# if non-transport modules (encryption, authentication) need to modify
	# some data between recv() and send(), this function should be used
	def modify_additional_data(self, additional_data, serverorclient):
		return additional_data

	# looking for client, based on the private IP
	def lookup_client_priv(self, msg):
		client_private_ip = msg[16:20]

		for c in self.clients:
			if c.get_private_ip_addr() == client_private_ip:
				return c

		return None

	# find client object based on public details (IP, identifier, etc.)
	def lookup_client_pub(self, additional_data):
		addr = additional_data[0]
		client_public_ip = socket.inet_aton(addr[0])

		for c in self.clients:
			if (c.get_public_ip_addr() == client_public_ip) and (c.get_public_src_port() == addr[1]):
				return c

		return None

	# find the encryption details of the client
	def get_client_encryption(self, additional_data):
		if self.serverorclient:
			# server mode
			# select client, based on public details
			c = self.lookup_client_pub(additional_data)
			if c:
				# if found, return object's encryption details
				return c.get_encryption()
			else:
				# this must be a new client, creating new encryption object
				e = encryption.Encryption_details()
				e.set_module(self.encryption_module)
				return e
		else:
			# client mode
			return self.encryption

	# set up the Client object for a new client
	def init_client(self, control_message, additional_data):
		addr = additional_data[0]
		client_local = client.Client()

		# stripping out the private IP from the message
		client_private_ip = control_message[0:4]
		# saving public IP address and port
		client_public_source_ip = socket.inet_aton(addr[0])
		client_public_source_port = addr[1]

		# If this private IP is already used, the server removes that client.
		# For example: client reconnect on connection reset, duplicated configs
		# and yes, this can be used to kick somebody off the tunnel

		# close client related pipes
		for c in self.clients:
			# find and remove client from module's list
			if c.get_private_ip_addr() == client_private_ip:
				save_to_close = c
				self.clients.remove(c)
				if c.get_pipe_r() in self.rlist:
					if self.os_type == common.OS_WINDOWS:
						# removing client related elements from lists
						idx = 0
						while self.rlist[idx] != c.get_pipe_r():
							idx += 1
						if idx < len(self.rlist):
							self.olist = self.olist[:idx] + self.olist[idx+1:]
							self.elist = self.elist[:idx] + self.elist[idx+1:]
							self.mlist = self.mlist[:idx] + self.mlist[idx+1:]
						else:
							self.olist = self.olist[:idx]
							self.elist = self.elist[:idx]
							self.mlist = self.mlist[:idx]

						self.ulist.remove(idx)
					self.rlist.remove(c.get_pipe_r())

		found = False
		for c in self.packetselector.get_clients():
			# find and remove client from packetselector's list
			if c.get_private_ip_addr() == client_private_ip:
				found = True
				self.packetselector.delete_client(c)

		# If client was created but not added to the PacketSelector, then the
		# pipes still need to be closed. This could happen when the authenti-
		# cation fails or gets interrupted.
		if not found:
			if self.os_type == common.OS_WINDOWS:
				import win32file

				try:
					win32file.CloseHandle(save_to_close.get_pipe_r())
					win32file.CloseHandle(save_to_close.get_pipe_w())
				except:
					pass
			else:
				try:
					save_to_close.get_pipe_r_fd().close()
					save_to_close.get_pipe_w_fd().close()
				except:
					pass


		# creating new pipes for the client
		if self.os_type == common.OS_WINDOWS:
			import win32pipe
			import win32file
			import pywintypes
			import win32event

			import win32api
			import winerror

			overlapped = pywintypes.OVERLAPPED()
			# setting up nameslot
			mailslotname = "\\\\.\\mailslot\\XFLTReaT_{0}".format(socket.inet_ntoa(client_private_ip))

			mailslot_r = win32file.CreateMailslot(mailslotname, 0, -1, None)
			if (mailslot_r == None) or (mailslot_r == win32file.INVALID_HANDLE_VALUE):
				internal_print("Invalid handle - mailslot", -1)
				sys.exit(-1)

			mailslot_w = win32file.CreateFile(mailslotname, win32file.GENERIC_WRITE,
				win32file.FILE_SHARE_READ | win32file.FILE_SHARE_WRITE, None, win32file.OPEN_EXISTING,
				win32file.FILE_ATTRIBUTE_NORMAL | win32file.FILE_FLAG_OVERLAPPED, None)
			if (mailslot_w == None) or (mailslot_w == win32file.INVALID_HANDLE_VALUE):
				internal_print("Invalid handle - readable pipe", -1)
				sys.exit(-1)

			client_local.set_pipes_fdnum(mailslot_r, mailslot_w)

		else:
			pipe_r, pipe_w = os.pipe()
			client_local.set_pipes_fdnum(pipe_r, pipe_w)
			client_local.set_pipes_fd(os.fdopen(pipe_r, "r"), os.fdopen(pipe_w, "w"))

		# set connection related things and authenticated to True
		client_local.set_public_ip_addr(client_public_source_ip)
		client_local.set_public_src_port(client_public_source_port)
		client_local.set_private_ip_addr(client_private_ip)

		client_local.get_encryption().set_module(self.encryption.get_module())
		self.encryption = client_local.get_encryption()

		if self.encryption.get_module().get_step_count():
			# add encryption steps
			self.merge_cmh(self.encryption.get_module().get_cmh_struct())

		if self.authentication.get_step_count():
			# add authentication steps
			self.merge_cmh(self.authentication.get_cmh_struct())

		client_local.set_initiated(True)
		self.clients.append(client_local)

		return

	# removing client from lists
	def remove_initiated_client(self, control_message, additional_data):
		c = self.lookup_client_pub(additional_data)
		if c:
			# from packetselector, clients list and rlist
			self.packetselector.delete_client(c)
			if c.get_authenticated():
				if self.os_type == common.OS_WINDOWS:
					# removing client related elements from lists
					idx = 0
					while self.rlist[idx] != c.get_pipe_r():
						idx += 1
					if idx < len(self.rlist):
						self.olist = self.olist[:idx] + self.olist[idx+1:]
						self.elist = self.elist[:idx] + self.elist[idx+1:]
						self.mlist = self.mlist[:idx] + self.mlist[idx+1:]
					else:
						self.olist = self.olist[:idx]
						self.elist = self.elist[:idx]
						self.mlist = self.mlist[:idx]
					if idx in self.ulist:
						self.ulist.remove(idx)
				self.rlist.remove(c.get_pipe_r())
			self.clients.remove(c)

		return

	# client side
	# after the init was done, this function is called 
	def post_init_client(self, control_message, additional_data):
		if not self.encryption.get_module().get_step_count():
			# no encryption
			self.post_encryption_client(control_message, additional_data)
		else:
			# add encryption steps
			self.merge_cmh(self.encryption.get_module().get_cmh_struct())
			# get and send encryption initialization message
			message = self.encryption.get_module().encryption_init_msg()
			self.send(common.CONTROL_CHANNEL_BYTE, message, self.modify_additional_data(additional_data, 0))

		return


	# server side
	# after the init was done, this function is called 
	# PLACEHOLDER for future needs
	def post_init_server(self, control_message, additional_data):

		return

	# client side
	# after the encryption part was done, this function is called
	def post_encryption_client(self, control_message, additional_data):
		if not self.authentication.get_step_count():
			# no encryption
			self.post_authentication_client()
		else:
			# add encryption steps
			self.merge_cmh(self.authentication.get_cmh_struct())
			# get and send encryption initialization message

			message = self.authentication.authentication_init_msg()
			self.send(common.CONTROL_CHANNEL_BYTE, message, self.modify_additional_data(additional_data, 0))
		

		return

	# server side
	# after the encryption part was done, this function is called
	# PLACEHOLDER for future needs
	def post_encryption_server(self, control_message, additional_data):

		return

	# client side
	# after the authentication part was done, this function is called
	def post_authentication_client(self, control_message, additional_data):
		self.authenticated = True

		return

	# server side
	# if the client was initiated (first step was not skipped), then set
	# the authenticated flag to True, add to packetselector and allow access
	# the read only pipe (packets selected for the client)
	def post_authentication_server(self, control_message, additional_data):
		c = self.lookup_client_pub(additional_data)
		if c.get_initiated():
			c.set_authenticated(True)
			self.packetselector.add_client(c)
			if c.get_pipe_r() not in self.rlist:
				self.rlist.append(c.get_pipe_r())
				if self.os_type == common.OS_WINDOWS:
					# creating objects and adding to corresponding lists
					import win32event
					import win32file
					import pywintypes

					hEvent_pipe = win32event.CreateEvent(None, 0, 0, None) # for reading from the pipe
					overlapped_pipe = pywintypes.OVERLAPPED()
					overlapped_pipe.hEvent = hEvent_pipe
					message_buffer = win32file.AllocateReadBuffer(4096)
					self.olist.append(overlapped_pipe)
					self.elist.append(hEvent_pipe)
					self.mlist.append(message_buffer)
					self.ulist.append(len(self.elist)-1)
			return True

		return False

	# PLACEHOLDER: prolog for the communication
	# What comes here: anything that should be set up before the actual
	# communication
	def communication_initialization(self):
		self.clients = []
		return

	# PLACEHOLDER: check function
	# What comes here: generate challenge and send to the server
	def do_check(self):

		return

	# PLACEHOLDER: authentication to the server
	# What comes here: generate authentication message and send to the server
	def do_hello(self):

		return

	# PLACEHOLDER: logoff function
	# What comes here: send message to the server about leaving
	def do_logoff(self):

		return

	# PLACEHOLDER: implementation of wrapping and sending message
	# What comes here: marking message (control or data), transforming (see
	# transform()), prepending with length, sending on the appropriate channel
	def send(self, channel_type, message, additional_data):

		return

	# PLACEHOLDER: implementation of recvieving and unpacking the message
	# What comes here: reading the first two bytes to determine length, reading
	# the full packet, transforming it to the original format (see transform())
	def recv(self):

		return

	# PLACEHOLDER: cleanup
	# What comes here: close() all sockets, do everything that should be done
	# to prepare for the exit process
	def cleanup(self):

		return

	# PLACEHOLDER: communication function
	# What comes here: this is the tricky part, where everything is handled 
	# that matters.
	# for unices
	def communication_unix(self, is_check):

		return

	# for windows
	def communication_win(self, is_check):

		return
