 /***************************************************************************
 *   Copyright (C) 2006 by Diego R. Brogna                                 *
 *   dierbro@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.                                   *
 *                                                                         *
 *   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.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.             *
 ***************************************************************************/
#include <tqtimer.h>
#include <tqcstring.h>
#include <tqdatetime.h>
#include <tdeapplication.h>
#include <kgenericfactory.h>
#include <tdeglobal.h>
#include <tdestandarddirs.h>
#include <kmdcodec.h>
#include <tdetempfile.h>

#include <tqfileinfo.h>
#include <tqsocket.h>
#include <tqstringlist.h>

#include <interfaces/coreinterface.h>
#include <interfaces/torrentinterface.h>

#include <util/log.h>		
#include <util/fileops.h>
#include <util/functions.h>
#include <util/mmapfile.h>
#include "ktversion.h"
#include "httpserver.h"
#include "httpclienthandler.h"
#include "httpresponseheader.h"
#include "php_handler.h"
#include "php_interface.h"
#include "webinterfacepluginsettings.h"

using namespace bt;

namespace kt
{
	
	

	HttpServer::HttpServer(CoreInterface *core, int port) : TQServerSocket(port, 5),core(core),cache(10,23)
	{
		php_i = new PhpInterface(core);
		clients.setAutoDelete(true);
		
		TQStringList dirList = TDEGlobal::instance()->dirs()->findDirs("data", "ktorrent/www");
		rootDir = *(dirList.begin());
		Out(SYS_WEB|LOG_DEBUG) << "WWW Root Directory "<< rootDir <<endl;
		session.logged_in = false;
		cache.setAutoDelete(true);
	}
	
	HttpServer::~HttpServer()
	{
		delete php_i;
	}

	void HttpServer::newConnection(int s)
	{
		TQSocket* socket = new TQSocket(this);
		socket->setSocket(s);
	
		connect(socket, TQ_SIGNAL(readyRead()), this, TQ_SLOT(slotSocketReadyToRead()));
		connect(socket, TQ_SIGNAL(delayedCloseFinished()), this, TQ_SLOT(slotConnectionClosed()));
		connect(socket, TQ_SIGNAL(connectionClosed()), this, TQ_SLOT(slotConnectionClosed()));
	
		HttpClientHandler* handler = new HttpClientHandler(this,socket);
		clients.insert(socket,handler);
		Out(SYS_WEB|LOG_NOTICE) << "connection from "<< TQString(socket->peerAddress().toString())  << endl;
	}
	

	void HttpServer::slotSocketReadyToRead()
	{
		TQSocket* client = (TQSocket*)sender();
		HttpClientHandler* handler = clients.find(client);
		if (!handler)
		{
			client->deleteLater();
			return;
		}
		
		handler->readyToRead();
	}
	
	static int DecodeEscapedChar(TQString & password,int idx)
	{
		TQChar a = password[idx + 1].lower();
		TQChar b = password[idx + 2].lower();
		if (!a.isNumber() && !(a.latin1() >= 'a' && a.latin1() <= 'f'))
			return idx + 2; // not a valid hex digit
		
		if (!b.isNumber() && !(b.latin1() >= 'a' && b.latin1() <= 'f'))
			return idx + 2; // not a valid hex digit
		
		// calculate high and low nibble
		Uint8 h = (a.latin1() - (a.isNumber() ? '0' : 'a')) << 4;
		Uint8 l = (b.latin1() - (b.isNumber() ? '0' : 'a'));
		char r = (char) h | l; // combine them and cast to a char
		password.replace(idx,3,r);
		return idx + 1;
	}
	
	bool HttpServer::checkLogin(const TQHttpRequestHeader & hdr,const TQByteArray & data)
	{
		if (hdr.contentType() != "application/x-www-form-urlencoded")
			return false;
		
		TQString username;
		TQString password;
		TQStringList params = TQStringList::split("&",TQString(data));
		for (TQStringList::iterator i = params.begin();i != params.end();i++)
		{
			TQString t = *i;
			if (t.section("=",0,0) == "username")
				username = t.section("=",1,1);
			else if (t.section("=",0,0) == "password")
				password = t.section("=",1,1);
			
			// check for passwords with url encoded stuff in them and decode them if necessary
			int idx = 0;
			while ((idx = password.find('%',idx)) > 0)
			{
				if (idx + 2 < password.length())
				{
					idx = DecodeEscapedChar(password,idx);
				}
				else
					break;
			}
		}

		if (!username.isNull() && !password.isNull())
		{
			KMD5 context(password.utf8());

			if(username == WebInterfacePluginSettings::username() && 
				context.hexDigest().data() == WebInterfacePluginSettings::password())
			{
				session.logged_in = true;
				session.sessionId=rand();
				session.last_access=TQTime::currentTime();
				Out(SYS_WEB|LOG_NOTICE) << "Webgui login succesfull !" << endl;
				return true;
			}
		}

		return false;
	}
	
	bool HttpServer::checkSession(const TQHttpRequestHeader & hdr)
	{
		// check session in cookie
		int session_id = 0;
		if (hdr.hasKey("Cookie"))
		{
			TQString cookie = hdr.value("Cookie");
			int idx = cookie.find("KT_SESSID=");
			if (idx == -1)
				return false;
			
			TQString number;
			idx += TQString("KT_SESSID=").length();
			while (idx < cookie.length())
			{
				if (cookie[idx] >= '0' && cookie[idx] <= '9')
					number += cookie[idx];
				else
					break;
				
				idx++;
			}
					
			session_id = number.toInt();
		}
		
	
		if (session_id == session.sessionId)
		{
			// check if the session hasn't expired yet
			if(session.last_access.secsTo(TQTime::currentTime())<WebInterfacePluginSettings::sessionTTL())
			{
				session.last_access=TQTime::currentTime();
			}
			else
			{
				return false;
			}
		}
		else
			return false;

		return true;
	}
	
	static TQString ExtensionToContentType(const TQString & ext)
	{
		if (ext == "html")
			return "text/html";
		else if (ext == "css")
			return "text/css";
		else if (ext == "js")
			return "text/javascript";
		else if (ext == "gif" || ext == "png" || ext == "ico")
			return "image/" + ext;
		else
			return TQString();
	}
	
	// HTTP needs non translated dates
	static TQString days[] = {
		"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"
	};
	
	static TQString months[] = {
		"Jan","Feb","Mar","Apr",
		"May","Jun","Jul","Aug",
		"Sep","Oct","Nov","Dec"
	};
	
	static TQString DateTimeToString(const TQDateTime & now,bool cookie)
	{
		if (!cookie)
			return now.toString("%1, dd %2 yyyy hh:mm:ss UTC")
				.arg(days[now.date().dayOfWeek() - 1])
				.arg(months[now.date().month() - 1]);
		else
			return now.toString("%1, dd-%2-yyyy hh:mm:ss GMT")
				.arg(days[now.date().dayOfWeek() - 1])
				.arg(months[now.date().month() - 1]);
	}
		
	void HttpServer::setDefaultResponseHeaders(HttpResponseHeader & hdr,const TQString & content_type,bool with_session_info)
	{
		hdr.setValue("Server","KTorrent/" KT_VERSION_MACRO);
		hdr.setValue("Date",DateTimeToString(TQDateTime::currentDateTime(TQt::UTC),false));
		hdr.setValue("Content-Type",content_type);
		hdr.setValue("Connection","keep-alive");
		if (with_session_info && session.sessionId && session.logged_in)
		{
			hdr.setValue("Set-Cookie",TQString("KT_SESSID=%1").arg(session.sessionId));
		}
	}
	
	void HttpServer::redirectToLoginPage(HttpClientHandler* hdlr)
	{
		HttpResponseHeader rhdr(301);
		setDefaultResponseHeaders(rhdr,"text/html",false);
		rhdr.setValue("Location","/login.html");
		TQString path = rootDir + bt::DirSeparator() + WebInterfacePluginSettings::skin() + "/login.html";
		if (!hdlr->sendFile(rhdr,path))
		{
			HttpResponseHeader nhdr(404);
			setDefaultResponseHeaders(nhdr,"text/html",false);
			hdlr->send404(nhdr,path);
		}
		Out(SYS_WEB|LOG_NOTICE) << "Redirecting to /login.html" << endl;
	}
	
	void HttpServer::handleGet(HttpClientHandler* hdlr,const TQHttpRequestHeader & hdr,bool do_not_check_session)
	{
		TQString file = hdr.path();
		if (file == "/")
			file = "/login.html";
		
		//Out(SYS_WEB|LOG_DEBUG) << "GET " << hdr.path() << endl;
		
		KURL url;
		url.setEncodedPathAndQuery(file);
		
		TQString path = rootDir + bt::DirSeparator() + WebInterfacePluginSettings::skin() + url.path();
		// first check if the file exists (if not send 404)
		if (!bt::Exists(path))
		{
			HttpResponseHeader rhdr(404);
			setDefaultResponseHeaders(rhdr,"text/html",false);
			hdlr->send404(rhdr,path);
			return;
		}
		
		TQFileInfo fi(path);
		TQString ext = fi.extension();
		
		// if it is the login page send that
		if (file == "/login.html" || file == "/")
		{
			session.logged_in = false;
			ext = "html";
			path = rootDir + bt::DirSeparator() + WebInterfacePluginSettings::skin() + "/login.html"; 
		}
		else if (!session.logged_in && (ext == "html" || ext == "php"))
		{
			// for any html or php page, a login is necessary
			redirectToLoginPage(hdlr);
			return;
		}
		else if (session.logged_in && !do_not_check_session && (ext == "html" || ext == "php"))
		{
			// if we are logged in and it's a html or php page, check the session id
			if (!checkSession(hdr))
			{
				session.logged_in = false;
				// redirect to login page
				redirectToLoginPage(hdlr);
				return;
			}
		}
		
		if (ext == "html")
		{
			HttpResponseHeader rhdr(200);
			setDefaultResponseHeaders(rhdr,"text/html",true);
			if (path.endsWith("login.html"))
			{
				// clear cookie in case of login page
				TQDateTime dt = TQDateTime::currentDateTime().addDays(-1);
				TQString cookie = TQString("KT_SESSID=666; expires=%1 +0000").arg(DateTimeToString(dt,true));
				rhdr.setValue("Set-Cookie",cookie);
			}
			
			if (!hdlr->sendFile(rhdr,path))
			{
				HttpResponseHeader nhdr(404);
				setDefaultResponseHeaders(nhdr,"text/html",false);
				hdlr->send404(nhdr,path);
			}
		}
		else if (ext == "css" || ext == "js" || ext == "png" || ext == "ico" || ext == "gif" || ext == "jpg")
		{
			if (hdr.hasKey("If-Modified-Since"))
			{
				TQDateTime dt = parseDate(hdr.value("If-Modified-Since"));
				if (dt.isValid() && dt < fi.lastModified())
				{	
					HttpResponseHeader rhdr(304);
					setDefaultResponseHeaders(rhdr,"text/html",true);
					rhdr.setValue("Cache-Control","max-age=0");
					rhdr.setValue("Last-Modified",DateTimeToString(fi.lastModified(),false));
					rhdr.setValue("Expires",DateTimeToString(TQDateTime::currentDateTime(TQt::UTC).addSecs(3600),false));
					hdlr->sendResponse(rhdr);
					return;
				}
			}
			
			
			HttpResponseHeader rhdr(200);
			setDefaultResponseHeaders(rhdr,ExtensionToContentType(ext),true);
			rhdr.setValue("Last-Modified",DateTimeToString(fi.lastModified(),false));
			rhdr.setValue("Expires",DateTimeToString(TQDateTime::currentDateTime(TQt::UTC).addSecs(3600),false));
			rhdr.setValue("Cache-Control","private");
			if (!hdlr->sendFile(rhdr,path))
			{
				HttpResponseHeader nhdr(404);
				setDefaultResponseHeaders(nhdr,"text/html",false);
				hdlr->send404(nhdr,path);
			}
		}
		else if (ext == "php")
		{
			bool redirect = false;
			bool shutdown = false;
			if (url.queryItems().count() > 0 && session.logged_in)
				redirect = php_i->exec(url,shutdown);
			
			if (shutdown)
			{
				// first send back login page
				redirectToLoginPage(hdlr);
				TQTimer::singleShot(1000,tdeApp,TQ_SLOT(quit()));
			}
			else if (redirect)
			{
				HttpResponseHeader rhdr(301);
				setDefaultResponseHeaders(rhdr,"text/html",true);
				rhdr.setValue("Location",url.encodedPathAndQuery());
				
				hdlr->executePHPScript(php_i,rhdr,WebInterfacePluginSettings::phpExecutablePath(),
									   path,url.queryItems());
			}
			else
			{
				HttpResponseHeader rhdr(200);
				setDefaultResponseHeaders(rhdr,"text/html",true);
			
				hdlr->executePHPScript(php_i,rhdr,WebInterfacePluginSettings::phpExecutablePath(),
								   path,url.queryItems());
			}
		}
		else
		{
			HttpResponseHeader rhdr(404);
			setDefaultResponseHeaders(rhdr,"text/html",false);
			hdlr->send404(rhdr,path);
		}
	}
	
	void HttpServer::handlePost(HttpClientHandler* hdlr,const TQHttpRequestHeader & hdr,const TQByteArray & data)
	{
		// this is either a file or a login
		if (hdr.value("Content-Type").startsWith("multipart/form-data"))
		{
			handleTorrentPost(hdlr,hdr,data);
		}
		else if (!checkLogin(hdr,data))
		{
			TQHttpRequestHeader tmp = hdr;
			tmp.setRequest("GET","/login.html",1,1);
			handleGet(hdlr,tmp);
		}
		else
		{
			handleGet(hdlr,hdr,true);
		}
	}
	
	void HttpServer::handleTorrentPost(HttpClientHandler* hdlr,const TQHttpRequestHeader & hdr,const TQByteArray & data)
	{
		const char* ptr = data.data();
		Uint32 len = data.size();
		int pos = TQString(data).find("\r\n\r\n");

		if (!session.logged_in || !checkSession(hdr))
		{
			// You can't post torrents if you are not logged in
			// or the session is not OK
			redirectToLoginPage(hdlr);
			return;
		}
		
		if (pos == -1 || pos + 4 >= len || ptr[pos + 4] != 'd')
		{
			HttpResponseHeader rhdr(500);
			setDefaultResponseHeaders(rhdr,"text/html",false);
			hdlr->send500(rhdr);
			return;
		}
		
		// save torrent to a temporary file
		KTempFile tmp_file(locateLocal("tmp", "ktwebgui-"), ".torrent");
		TQDataStream* out = tmp_file.dataStream();
		if (!out)
		{
			HttpResponseHeader rhdr(500);
			setDefaultResponseHeaders(rhdr,"text/html",false);
			hdlr->send500(rhdr);
			return;
		}
		
		out->writeRawBytes(ptr + (pos + 4),len - (pos + 4));
		tmp_file.sync();
		tmp_file.setAutoDelete(true);
		
		Out(SYS_WEB|LOG_NOTICE) << "Loading file " << tmp_file.name() << endl;
		core->loadSilently(KURL::fromPathOrURL(tmp_file.name()));
		
		handleGet(hdlr,hdr);
	}
	
	void HttpServer::handleUnsupportedMethod(HttpClientHandler* hdlr)
	{
		HttpResponseHeader rhdr(500);
		setDefaultResponseHeaders(rhdr,"text/html",false);
		hdlr->send500(rhdr);
	}
	
	void HttpServer::slotConnectionClosed()
	{
		TQSocket* socket= (TQSocket*)sender();
		clients.erase(socket);
	}

	TQDateTime HttpServer::parseDate(const TQString & str)
	{
		/*
		Potential date formats :
		    Sun, 06 Nov 1994 08:49:37 GMT  ; RFC 822, updated by RFC 1123
      		Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
      		Sun Nov  6 08:49:37 1994       ; ANSI C's asctime() format
		*/
		TQStringList sl = TQStringList::split(" ",str);
		if (sl.count() == 6)
		{
			// RFC 1123 format
			TQDate d;
			TQString month = sl[2];
			int m = -1;
			for (int i = 1;i <= 12 && m < 0;i++)
				if (TQDate::shortMonthName(i) == month)
					m = i;
			
			d.setYMD(sl[3].toInt(),m,sl[1].toInt());
			
			TQTime t = TQTime::fromString(sl[4],TQt::ISODate);
			return TQDateTime(d,t);
		}
		else if (sl.count() == 4)
		{
			//  RFC 1036
			TQStringList dl = TQStringList::split("-",sl[1]);
			if (dl.count() != 3)
				return TQDateTime();
			
			TQDate d;
			TQString month = dl[1];
			int m = -1;
			for (int i = 1;i <= 12 && m < 0;i++)
				if (TQDate::shortMonthName(i) == month)
					m = i;
			
			d.setYMD(2000 + dl[2].toInt(),m,dl[0].toInt());
			
			TQTime t = TQTime::fromString(sl[2],TQt::ISODate);
			return TQDateTime(d,t);
		}
		else if (sl.count() == 5)
		{
			// ANSI C
			TQDate d;
			TQString month = sl[1];
			int m = -1;
			for (int i = 1;i <= 12 && m < 0;i++)
				if (TQDate::shortMonthName(i) == month)
					m = i;
			
			d.setYMD(sl[4].toInt(),m,sl[2].toInt());
			
			TQTime t = TQTime::fromString(sl[3],TQt::ISODate);
			return TQDateTime(d,t);
		}
		else
			return TQDateTime();
	}
	
	bt::MMapFile* HttpServer::cacheLookup(const TQString & name)
	{
		return cache.find(name);
	}
	
	void HttpServer::insertIntoCache(const TQString & name,bt::MMapFile* file)
	{
		cache.insert(name,file);
	}

}

#include "httpserver.moc"
