#
#	Copyright 2008-2013 Clipperz Srl
#	
#	This file is part of Clipperz, the online password manager.
#	For further information about its features and functionalities please
#	refer to http://www.clipperz.com.
#	
#	* Clipperz is free software: you can redistribute it and/or modify it 
#     under the terms of the GNU Affero General Public License as published
#     by the Free Software Foundation, either version 3 of the License, or 
#     (at your option) any later version.
#	
#	* Clipperz 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 Affero General Public License for more details.
#	
#	* You should have received a copy of the GNU Affero General Public
#	  License along with Clipperz. If not, see http://www.gnu.org/licenses/.
#

import os
import cgi
import wsgiref.handlers

import datetime
import uuid
import random
import hashlib

import logging

from google.appengine.api import users
from google.appengine.ext import webapp
from google.appengine.ext import db
from google.appengine.ext.webapp import template

from django.utils import simplejson

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

sessionTimeout = datetime.timedelta(minutes=-2)

def randomSeed():
	return hex(random.getrandbits(32*8))[2:-1]

def clipperzHash(aString):
	#logging.info(">>> string: " + aString)
	firstRound = hashlib.sha256()
	firstRound.update(aString)
	#logging.info("firstRound: " + firstRound.hexdigest() + " - " + firstRound.digest())
	result = hashlib.sha256()
	result.update(firstRound.digest())
	#logging.info("<<< finalResul: " + result.hexdigest())

	return result.hexdigest()
	
#==============================================================================

class User(db.Model):
	username	= db.StringProperty()
	srp_s		= db.StringProperty()
	srp_v		= db.StringProperty()
	header		= db.TextProperty()
	statistics	= db.TextProperty()
	auth_version= db.StringProperty()
	version		= db.StringProperty()
	lock		= db.StringProperty()
	
	def updateCredentials(self, someCredentials):
		self.username		= someCredentials['C']
		self.srp_s			= someCredentials['s']
		self.srp_v			= someCredentials['v']
		self.auth_version	= someCredentials['version']

	def update(self, someData):
		self.header		= someData['header']
		self.statistics	= someData['statistics']
		self.version	= someData['version']
		self.lock		= someData['lock']

#------------------------------------------------------------------------------

class Record(db.Model):
	user			= db.ReferenceProperty(User)
	reference		= db.StringProperty()
	data			= db.TextProperty()
	version			= db.StringProperty()
	creation_date	= db.DateTimeProperty(auto_now_add=True)
	update_date		= db.DateTimeProperty(auto_now_add=True)
	access_date		= db.DateTimeProperty(auto_now_add=True)

#------------------------------------------------------------------------------

class RecordVersion(db.Model):
	record				= db.ReferenceProperty(Record)
	reference			= db.StringProperty()
	header				= db.TextProperty()
	data				= db.TextProperty()
	version				= db.StringProperty()
	previousVersionKey	= db.StringProperty()
	previousVersion		= db.SelfReferenceProperty()
	creation_date		= db.DateTimeProperty(auto_now_add=True)
	update_date			= db.DateTimeProperty(auto_now_add=True)
	access_date			= db.DateTimeProperty(auto_now_add=True)

	def update(self, someData):
		recordData = someData['record'];
		self.parent().reference =	recordData['reference']
		self.parent().data =		recordData['data']
		self.parent().version =		recordData['version']
		self.parent().update_date =	datetime.datetime.now()

		recordVersionData = someData['currentRecordVersion'];
		self.reference =			recordVersionData ['reference']
		self.data =					recordVersionData ['data']
		self.version =				recordVersionData ['version']
		#self.previous_version	=	#recordVersionData ['previousVersion']
		self.previous_version_key =	recordVersionData ['previousVersionKey']
		self.update_date =			datetime.datetime.now()

#------------------------------------------------------------------------------

class OneTimePassword(db.Model):
	user			= db.ReferenceProperty(User)
	status			= db.StringProperty()
	reference		= db.StringProperty()
	keyValue		= db.StringProperty()
	keyChecksum		= db.StringProperty()
	data			= db.TextProperty()
	version			= db.StringProperty()
	creation_date	= db.DateTimeProperty(auto_now_add=True)
	request_date	= db.DateTimeProperty()
	usage_date		= db.DateTimeProperty()

	def update(self, someParameters, aStatus):
		self.reference =		someParameters['reference']
		self.keyValue =			someParameters['key']
		self.keyChecksum =		someParameters['keyChecksum']
		self.data =				someParameters['data']
		self.version =			someParameters['version']
		self.status =			aStatus

	def reset(self, aStatus):
		self.data =		""
		self.status =	aStatus

		return self

#------------------------------------------------------------------------------

class Session(db.Expando):
	sessionId	= db.StringProperty()
	access_date	= db.DateTimeProperty()

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

class MainPage(webapp.RequestHandler):
	def get(self):
		path = os.path.join(os.path.dirname(__file__), 'static%s' % self.request.path)
		self.response.out.write(template.render(path, {}))

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

class XHR(webapp.RequestHandler):

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

	def get(self):
		logging.info("self.request.path: " + self.request.path)
		if self.request.path == "/dump":
			session = self.getSession()
			userData = {}
			offline_data_placeholder = ""
		
			user = db.Query(User).filter('username =', session.C).get()
		
			userData['users'] = {
				'catchAllUser': {
					'__masterkey_test_value__': 'masterkey',
					's': '112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00',
					'v': '112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00'
				}
			}

			records = {}
			for currentRecord in db.Query(Record).ancestor(user):
				versions = {}
				for currentVersion in db.Query(RecordVersion).ancestor(currentRecord):
					versions[currentVersion.reference] ={
						'header':		currentVersion.header,
						'data':			currentVersion.data,
						'version':		currentVersion.version,
						'creationDate':	str(currentVersion.creation_date),
						'updateDate':	str(currentVersion.update_date),
						'accessDate':	str(currentVersion.access_date)
					}

				records[currentRecord.reference] = {
					'data':				currentRecord.data,
					'version':			currentRecord.version,
					'creationDate':		str(currentRecord.creation_date),
					'updateDate':		str(currentRecord.update_date),
					'accessDate':		str(currentRecord.access_date),
					'currentVersion':	currentVersion.reference,
					'versions':			versions
				}

			userData['users'][user.username] = {
				's':					user.srp_s,
				'v':					user.srp_v,
				'version':				user.auth_version,
				'maxNumberOfRecords':	'100',
				'userDetails':			user.header,
				'statistics':			user.statistics,
				'userDetailsVersion':	user.version,
				'records':				records
			}

			offline_data_placeholder = offline_data_placeholder + "_clipperz_dump_data_ = " + simplejson.dumps(userData, indent=4) + "\n"
			offline_data_placeholder = offline_data_placeholder + "Clipperz.PM.Proxy.defaultProxy = new Clipperz.PM.Proxy.Offline();" + "\n"
			offline_data_placeholder = offline_data_placeholder + "Clipperz.Crypto.PRNG.defaultRandomGenerator().fastEntropyAccumulationForTestingPurpose();" + "\n"

			path = os.path.join(os.path.dirname(__file__), 'static/dump.html')

			self.response.headers.add_header('Content-Type', 'text/html')
			self.response.headers.add_header('Content-Disposition', 'attachment', filename='Clipperz.html')
			self.response.out.write(template.render(path, {'offline_data_placeholder': offline_data_placeholder}))

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

	def post(self):
		method = self.request.get('method')
		parameters = simplejson.loads(self.request.get('parameters'))
		session = self.getSession()
		result = {};

		#----------------------------------------------------------------------

		if method == 'registration':
			message = parameters['message'];
			
			if message == 'completeRegistration':
				user = User()
			
				user.updateCredentials(parameters['credentials'])
				user.update(parameters['user'])
				user.put()

				result['lock'] = user.lock
				result['result'] = "done"

		#----------------------------------------------------------------------

		elif method == 'handshake':
			srp_g = 2L
			srp_n = long("0x%s" % "115b8b692e0e045692cf280b436735c77a5a9e8a9e7ed56c965f87db5b2a2ece3", 16)

			message = parameters['message'];

			#------------------------------------------------------------------

			if message == 'connect':
				session.C = parameters['parameters']['C']
				session.A = parameters['parameters']['A']

				user = db.Query(User).filter('username =', session.C).get()

				if user != None:
					try:
						optId = session.otpId

						oneTimePassword = db.Query(OneTimePassword).filter('keyValue =', optId).get()
						
						if oneTimePassword.parent().username != user.username:
							oneTimePassword.reset('DISABLED').put()
							raise Exception, "User missmatch between the current session and 'One Time Password' user"
						elif oneTimePassword.status != 'REQUESTED':
							oneTimePassword.reset('DISABLED').put()
							raise Exception, "Tring to use an 'One Time Password' in the wrong state"

						oneTimePassword.reset("USED").put()

						result['oneTimePassword'] = oneTimePassword.reference

					except Exception, detail:
						logging.error("connect.optId: " + str(detail))

					session.s = user.srp_s
					session.v = user.srp_v
				else:
					session.s = "112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00"
					session.v = "112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00"

				session.b = randomSeed()
				session.B = hex(long("0x%s" % session.v, 16) + pow(srp_g, long("0x%s" %session.b, 16), srp_n))[2:-1]

				result['s'] = session.s
				result['B'] = session.B

			#------------------------------------------------------------------

			elif message == 'credentialCheck':
				B = long("0x%s" % session.B, 16)
				b = long("0x%s" % session.b, 16)
				A = long("0x%s" % session.A, 16)
				v = long("0x%s" % session.v, 16)
				u = long("0x%s" % clipperzHash(str(B)), 16)
				n = srp_n

				S  = pow((A * pow(v, u, n)), b, n)
				K  = clipperzHash(str(S))
				M1 = clipperzHash(str(A) + str(B) + K)

				if M1 == parameters['parameters']['M1']:
					session.K = K
					M2 = clipperzHash(str(A) + M1 + K)

					result['M2'] = M2
					result["connectionId"] = ""
					result["loginInfo"] = {}
					result["loginInfo"]["latest"] = {}
					result["loginInfo"]["current"] = {}
					result["offlineCopyNeeded"] = "false";
					result["lock"] = "----";
				else:
					result['error'] = "?"

			#------------------------------------------------------------------

			elif message == 'oneTimePassword':
				oneTimePassword = db.Query(OneTimePassword).filter("keyValue =", parameters["parameters"]["oneTimePasswordKey"]).get()

				if oneTimePassword != None:
					if oneTimePassword.status == 'ACTIVE':
						if oneTimePassword.keyChecksum == parameters['parameters']['oneTimePasswordKeyChecksum']:
							#session.userId =	str(oneTimePassword.parent().username)
							session.otpId =		str(oneTimePassword.keyValue)
							
							result['data'] = oneTimePassword.data
							result['version'] = oneTimePassword.version

							oneTimePassword.reset('REQUESTED').put()

						else:
							oneTimePassword.reset('DISABLED').put()
							raise Exception, "The requested One Time Password has been disabled, due to a wrong keyChecksum"
					else:
						raise Exception, "The requested One Time Password was not active"
				else:
					raise Exception, "The requested One Time Password has not been found"

		#----------------------------------------------------------------------

		elif method == 'message':
			if parameters['srpSharedSecret'] == session.K:
				message = parameters['message']

				if message == 'getUserDetails':
					#	{"message":"getUserDetails", "srpSharedSecret":"f18e5cf7c3a83b67d4db9444af813ee48c13daf4f8f6635397d593e52ba89a08", "parameters":{}}
					user = db.Query(User).filter('username =', session.C).get()
					
					result['header'] =		user.header;
					result['statistics'] =	user.statistics;
					result['version'] =		user.version;

				elif 	message == "addNewRecords":
					user = db.Query(User).filter('username =', session.C).get()
					result = db.run_in_transaction(self.addNewRecords, session, user, parameters)

					"""
					user = db.Query(User).filter('username =', session.C).get()
					user.update(parameters['parameters']['user'])
					
					for recordParameter in parameters['parameters']['records']:
						record = Record(parent=user)
						record.put()
						recordVersion = RecordVersion(parent=record)
						recordVersion.put()
						
						recordVersion.update(recordParameter)

						record.put()
						recordVersion.put()

					user.put();

					result['lock'] = user.lock
					result['result'] = 'done'
					"""
					
				elif message == 'getRecordDetail':
					record = db.Query(Record).ancestor(db.Query(User).filter('username =', session.C).get()).filter('reference =', parameters["parameters"]["reference"]).get()
					recordVersion = db.Query(RecordVersion).ancestor(record).get()

					result['currentVersion'] = {}
					result['currentVersion']['reference'] =		recordVersion.reference
					result['currentVersion']['data'] =			recordVersion.data
					result['currentVersion']['header'] =		recordVersion.header
					result['currentVersion']['version'] =		recordVersion.version
					result['currentVersion']['creationDate'] =	str(recordVersion.creation_date)
					result['currentVersion']['updateDate'] =	str(recordVersion.update_date)
					result['currentVersion']['accessDate'] =	str(recordVersion.access_date)

					result['reference'] =						record.reference
					result['data'] =							record.data
					result['version'] =							record.version
					result['creationDate'] =					str(record.creation_date)
					result['updateDate'] =						str(record.update_date)
					result['accessDate'] =						str(record.access_date)
					result['oldestUsedEncryptedVersion'] =		"---"

				elif message == 'updateData':
					user = db.Query(User).filter('username =', session.C).get()
					user.update(parameters['parameters']['user'])

					for recordParameter in parameters['parameters']['records']:
						logging.info('reference =' + recordParameter['record']['reference'])
						record = db.Query(Record).ancestor(user).filter('reference =', recordParameter['record']['reference']).get()
						recordVersion = db.Query(RecordVersion).ancestor(record).get()
						
						recordVersion.update(recordParameter)

						recordVersion.put()
						recordVersion.parent().put()

					user.put();

					result['lock'] = user.lock
					result['result'] = 'done'

			 	elif message == 'deleteRecords':
					user = db.Query(User).filter('username =', session.C).get()
					user.update(parameters['parameters']['user'])

					for recordReference in parameters['parameters']['recordReferences']:
						record = db.Query(Record).ancestor(user).filter('reference =', recordReference).get()
						#recordVersion = db.Query(RecordVersion).ancestor(record).get()
						
						db.delete(db.Query(RecordVersion).ancestor(record))
						record.delete()
						
					user.put()

					result['lock'] = user.lock
					result['result'] = 'done'

				elif message == 'deleteUser':
					user = db.Query(User).filter('username =', session.C).get()
					db.delete(db.Query(RecordVersion).ancestor(user))
					db.delete(db.Query(Record).ancestor(user))
					user.delete()

				elif message == 'addNewOneTimePassword':
					user = db.Query(User).filter('username =', session.C).get()
					user.update(parameters['parameters']['user'])

					oneTimePassword = OneTimePassword(parent=user)
					oneTimePassword.update(parameters['parameters']['oneTimePassword'], "ACTIVE")
					oneTimePassword.put()
					
					user.put()

					result['lock'] = user.lock
					result['result'] = 'done'

				elif message == 'updateOneTimePasswords':
					user = db.Query(User).filter('username =', session.C).get()
					user.update(parameters['parameters']['user'])

					validOtpReferences = parameters['parameters']['oneTimePasswords']
					for currentOtp in db.Query(OneTimePassword).ancestor(user):
						if currentOtp.reference in validOtpReferences:
							pass
						else:
							currentOtp.delete()

					user.put()

					result['result'] = user.lock

				elif message == 'getOneTimePasswordsDetails':
					pass

				elif message == 'getLoginHistory':
					result["result"] = []

				elif message == 'upgradeUserCredentials':
					user = db.Query(User).filter('username =', session.C).get()

					user.updateCredentials(parameters['parameters']['credentials'])
					user.update(parameters['parameters']['user'])

					for oneTimePasswordReference in parameters['parameters']['oneTimePasswords']:
						oneTimePassword = db.Query(OneTimePassword).ancestor(user).filter("reference =", oneTimePasswordReference).get()
						
						if oneTimePassword != None:
							oneTimePassword.data = parameters['parameters']['oneTimePasswords'][oneTimePasswordReference]
							oneTimePassword.put()

					user.put()
					
					result['lock'] = user.lock
					result['result'] = 'done'
					
					"""
					$user = new user();
					$user->Get($_SESSION["userId"]);

					$otp = new onetimepassword();
					
					updateUserCredentials($parameters["parameters"]["credentials"], $user);
					updateUserData($parameters["parameters"]["user"], $user);

					$otpList = $parameters["parameters"]["oneTimePasswords"];
					foreach($otpList as $otpReference=>$otpData) {
						$otpList = $otp->GetList(array(array("reference", "=", $otpReference)));
						$currentOtp = $otpList[0];
						$currentOtp->data = $otpData;
						$currentOtp->Save();
					}

					$user->Save();

					$result["lock"] = $user->lock;
					$result["result"] = "done";
					"""

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

					"""
					java.util.Map	result;
		
					try {
						java.util.Map	credentials;
		
						if (someParameters.get("credentials") != null) {
							credentials = (java.util.Map)someParameters.get("credentials");
						} else {
							credentials = someParameters;
						}

						aUser.setUsername((java.lang.String)credentials.get("C"));
						aUser.setSrpS((java.lang.String)credentials.get("s"));
						aUser.setSrpV((java.lang.String)credentials.get("v"));
						aUser.setVersion((java.lang.String)credentials.get("version"));

						if (someParameters.get("user") != null) {
							com.clipperz.dataModel.EncoderHelper.updateWithMap(aUser, (java.util.Map)someParameters.get("user"));
						}

						if (someParameters.get("oneTimePasswords") != null) {
							java.util.Map	updatedOneTimePasswords;
							java.util.List	usersOneTimePasswords;
							int i,c;
				
							updatedOneTimePasswords = (java.util.Map)someParameters.get("oneTimePasswords");
							usersOneTimePasswords = com.clipperz.dataModel.OneTimePassword.oneTimePasswordsForUser(this.user());
							c = usersOneTimePasswords.size();
							for (i=0; i<c; i++) {
								com.clipperz.dataModel.OneTimePassword	currentOneTimePassword;
					
								currentOneTimePassword = (com.clipperz.dataModel.OneTimePassword)usersOneTimePasswords.get(i);
					
								if (updatedOneTimePasswords.get(currentOneTimePassword.getReference()) != null) {
									currentOneTimePassword.setData((java.lang.String)updatedOneTimePasswords.get(currentOneTimePassword.getReference()));
								}
							}
						}
			
						result = new java.util.Hashtable();
						this.dataContext().commitChanges();
						result.put("lock", this.user().getNewLock());
						result.put("result", "done");
					} catch(java.lang.Exception exception) {
						this.dataContext().rollbackChanges();
						logger.error(exception);
						throw exception;
					}
		
					return result;
					"""

				elif message == 'echo':
					result['result'] = parameters;

			else:
				result['error'] = "Wrong shared secret!"

		#----------------------------------------------------------------------

		elif method == 'logout':
			result['method'] = 'logout'

		#----------------------------------------------------------------------

		else:
			result['method'] = 'PRRRRRR'

		#----------------------------------------------------------------------

		self.saveSession(session)
		self.response.out.write(simplejson.dumps(result))

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

	def addNewRecords (self, aSession, aUser, someParameters):
		result = {}
		
		#user = db.Query(User).filter('username =', aSession.C).get()
		aUser.update(someParameters['parameters']['user'])
		
		for recordParameter in someParameters['parameters']['records']:
			record = Record(parent=aUser)
			record.put()
			recordVersion = RecordVersion(parent=record)
			recordVersion.put()
			
			recordVersion.update(recordParameter)

			record.put()
			recordVersion.put()

		aUser.put();

		result['lock'] = aUser.lock
		result['result'] = 'done'
		
		return result
	
	#==========================================================================
	
	def getSession(self):
		#logging.info(">>> getSession (%d) => %s" % (db.Query(Session).count(), str(map(lambda v: v.sessionId, db.Query(Session).fetch(100)))) )
		result = None
		try:
			sessionId = self.request.cookies['sessionId']
		except:
			sessionId = None

		#logging.info("wannabe sessionId: " + str(sessionId))
		
		if sessionId != None:
			#query = db.Query(Session)
			#query.filter('sessionId =', sessionId)

			#result = query.get()
			
			#result = db.Query(Session).filter('sessionId =', str(sessionId)).filter('access_date >', (datetime.datetime.utcnow() - sessionTimeout)).get()
			result = db.Query(Session).filter('sessionId =', str(sessionId)).get()
			#logging.info("searching session on datastore. Found: " + str(result))

		if result == None:
			sessionId = str(uuid.uuid4())
			#logging.info("creating a new session with sessionId=" + str(sessionId))
			result = Session(sessionId=sessionId)

		result.access_date = datetime.datetime.utcnow()
		result.put()
		
		#logging.info("<<< getSession (%d)" % db.Query(Session).count())
		
		return result

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

	def saveSession(self, aSession):
		#logging.info(">>> saveSession (%d)" % db.Query(Session).count())
		#self.response.set_cookie('sessionId', aSession.sessionId, max_age=360, path='/', domain='example.org', secure=True)
		aSession.put()
		self.response.headers.add_header('Set-Cookie', 'sessionId=' + str(aSession.sessionId), path='/')
		self.cleanOldSessions()
		#logging.info("<<< saveSession (%d)" % db.Query(Session).count())

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

	def cleanOldSessions(self):
		query = db.Query(Session).filter('accessDate <', (datetime.datetime.utcnow() - sessionTimeout))

		expiredSessions = query.count();
		if expiredSessions != 0:
			#logging.info("deleting %d sessions" % expiredSessions)
			pass
			
		"""
		try:
			db.delete(query)
		except Exception, exception:
			logging.error("some issues raised while deleting the expired sessions")
			logging.error("exception type: " + str(type(exception)))
			logging.error("exception: " + str(exception))
		"""
		pass

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

def main():
	application = webapp.WSGIApplication([('/xhr', XHR), ('/dump', XHR), ('/.*', MainPage)], debug=True)
	wsgiref.handlers.CGIHandler().run(application)

if __name__ == "__main__":
	main()