/* "CodeWorker":	a scripting language for parsing and generating text.

Copyright (C) 1996-1997, 1999-2003 Cédric Lemaire

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library 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
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

To contact the author: codeworker@free.fr
*/

#ifdef WIN32
#	pragma warning(disable : 4503)
#	pragma warning(disable : 4786)
#	include <rpc.h>
#endif

#ifdef CODEWORKER_CURL_LIB
#	include "include/curl/curl.h"
#endif

#include "UtlException.h"
#include "CGRuntime.h"

#include "DtaScriptVariable.h"
#include "HTTPRequest.h"


namespace CodeWorker {

#ifdef CODEWORKER_CURL_LIB
	struct CurlOutputDataSection {
		char* _tcSection;
		int   _iLength;
		CurlOutputDataSection* next;

		CurlOutputDataSection(const char* tcSection, int iLength) : _iLength(iLength), next(0L) {
			_tcSection = new char[_iLength];
			memcpy(_tcSection, tcSection, _iLength);
		}
		~CurlOutputDataSection() {
			delete [] _tcSection;
			delete next;
		}
	};

	struct CurlOutputData {
		CurlOutputDataSection* first;
		CurlOutputDataSection* last;
		std::map<std::string, std::list<std::string> > headers;

		inline CurlOutputData() : first(0L), last(0L) {}
		~CurlOutputData() { delete first; }

		int length() const {
			int iLength = 0;
			CurlOutputDataSection* pCurrent = first;
			while (pCurrent != 0L) {
				iLength += pCurrent->_iLength;
				pCurrent = pCurrent->next;
			}
			return iLength;
		}

		char* array() const {
			if (first == 0L) return 0L;
			char* tcArray = new char[length()];
			CurlOutputDataSection* pCurrent = first;
			int iLength = 0;
			while (pCurrent != 0L) {
				memcpy(tcArray + iLength, pCurrent->_tcSection, pCurrent->_iLength);
				iLength += pCurrent->_iLength;
				pCurrent = pCurrent->next;
			}
			return tcArray;
		}

		bool isBinary() const {
			CurlOutputDataSection* pCurrent = first;
			while (pCurrent != 0L) {
				char* tcSection = pCurrent->_tcSection;
				for (int i = 0; i < pCurrent->_iLength; i++) {
					unsigned char a = (unsigned char) *tcSection;
					if ((a == 255) || (a <= 1)) return true;
					tcSection++;
				}
				pCurrent = pCurrent->next;
			}
			return false;
		}
	};

	size_t __cdecl writeHeaderLineFunction(void* ptr, size_t iSize, size_t iNmemb, void* pCurlOutput) {
		unsigned int iLength = iSize*iNmemb;
		CurlOutputData* pData = (CurlOutputData*) pCurlOutput;
		const char* tcHeader = (const char*) ptr;
		unsigned int iIndex = 0;
		while ((iIndex < iLength) && (tcHeader[iIndex] != ':')) iIndex++;
		std::string sHeader = std::string(tcHeader, iIndex);
		if (iIndex < iLength) {
			do {
				iIndex++;
			} while ((iIndex < iLength) && (tcHeader[iIndex] == ' '));
		}
		if (iIndex < iLength) {
			std::string sValue = std::string(tcHeader + iIndex, iLength - iIndex);
			std::string::size_type iEnd = sValue.find_first_of("\r\n");
			if (iEnd != std::string::npos) sValue = sValue.substr(0, iEnd);
			pData->headers[sHeader].push_back(sValue);
		} else {
			std::string::size_type iEnd = sHeader.find_first_of("\r\n");
			if (iEnd != std::string::npos) sHeader = sHeader.substr(0, iEnd);
			if (!sHeader.empty()) {
				pData->headers[sHeader].push_back("");
			}
		}
		return iLength;
	}

	size_t __cdecl writeCurlFunction(void* ptr, size_t iSize, size_t iNmemb, void* pCurlOutput) {
		int iLength = iSize*iNmemb;
		CurlOutputData* pData = (CurlOutputData*) pCurlOutput;
		CurlOutputDataSection* pSection = new CurlOutputDataSection((const char*) ptr, iLength);
		if (pData->last == 0L) pData->first = pSection;
		else pData->last->next = pSection;
		pData->last = pSection;
		return iLength;
	}

	std::string getCurlErrorCode(CURLcode aCode) {
		switch(aCode) {
			case CURLE_OK: return "";
			case CURLE_UNSUPPORTED_PROTOCOL: return "Unsupported protocol";
			case CURLE_FAILED_INIT: return "Failed init";
			case CURLE_URL_MALFORMAT: return "URL malformated";
			case CURLE_URL_MALFORMAT_USER: return "URL malformated - user";
			case CURLE_COULDNT_RESOLVE_PROXY: return "Couldn't resolve proxy";
			case CURLE_COULDNT_RESOLVE_HOST: return "Couldn't resolve host";
			case CURLE_COULDNT_CONNECT: return "Couldn't connect";
			case CURLE_FTP_WEIRD_SERVER_REPLY: return "FTP weird server reply";
			case CURLE_FTP_ACCESS_DENIED: return "FTP access denied";
			case CURLE_FTP_USER_PASSWORD_INCORRECT: return "FTP password incorrect";
			case CURLE_FTP_WEIRD_PASS_REPLY: return "FTP weird password reply";
			case CURLE_FTP_WEIRD_USER_REPLY: return "FTP weird user reply";
			case CURLE_FTP_WEIRD_PASV_REPLY: return "FTP weird PASV reply";
			case CURLE_FTP_WEIRD_227_FORMAT: return "FTP weird 227 format";
			case CURLE_FTP_CANT_GET_HOST: return "FTP can't get host";
			case CURLE_FTP_CANT_RECONNECT: return "FTP can't reconnect";
			case CURLE_FTP_COULDNT_SET_BINARY: return "FTP can't set binary";
			case CURLE_PARTIAL_FILE: return "FTP partial file";
			case CURLE_FTP_COULDNT_RETR_FILE: return "FTP couldn't retrieve file";
			case CURLE_FTP_WRITE_ERROR: return "FTP write error";
			case CURLE_FTP_QUOTE_ERROR: return "FTP quote error";
			case CURLE_HTTP_RETURNED_ERROR: return "FTP returned error";
			case CURLE_WRITE_ERROR: return "FTP write error";
			case CURLE_MALFORMAT_USER: return "User name is illegally specified";
			case CURLE_FTP_COULDNT_STOR_FILE: return "Failed FTP upload";
			case CURLE_READ_ERROR: return "Couldn't open/read from file";
			case CURLE_OUT_OF_MEMORY: return "Out of memory";
			case CURLE_OPERATION_TIMEOUTED: return "The timeout time was reached";
			case CURLE_FTP_COULDNT_SET_ASCII: return "FTP couldn't set ASCII";
			case CURLE_FTP_PORT_FAILED: return "FTP PORT operation failed";
			case CURLE_FTP_COULDNT_USE_REST: return "FTP couldn't use REST";
			case CURLE_FTP_COULDNT_GET_SIZE: return "FTP couldn't get size";
			case CURLE_HTTP_RANGE_ERROR: return "HTTP RANGE 'command' didn't work";
			case CURLE_HTTP_POST_ERROR: return "HTTP POST error";
			case CURLE_SSL_CONNECT_ERROR: return "HTTP SSL connection error";
			case CURLE_BAD_DOWNLOAD_RESUME: return "Bad download resume";
			case CURLE_FILE_COULDNT_READ_FILE: return "Couldn't read file";
			case CURLE_LDAP_CANNOT_BIND: return "LDAP cannot bind";
			case CURLE_LDAP_SEARCH_FAILED: return "LDAP search failed";
			case CURLE_LIBRARY_NOT_FOUND: return "Library not found";
			case CURLE_FUNCTION_NOT_FOUND: return "Function not found";
			case CURLE_ABORTED_BY_CALLBACK: return "Aborted by callback";
			case CURLE_BAD_FUNCTION_ARGUMENT: return "Bad function argument";
			case CURLE_BAD_CALLING_ORDER: return "Bad calling order";
			case CURLE_HTTP_PORT_FAILED: return "HTTP Interface operation failed";
			case CURLE_BAD_PASSWORD_ENTERED: return "Bad password entered";
			case CURLE_TOO_MANY_REDIRECTS: return "Catch endless re-direct loops";
			case CURLE_UNKNOWN_TELNET_OPTION: return "Unknown TELNET option";
			case CURLE_TELNET_OPTION_SYNTAX: return "Malformed TELNET option";
			case CURLE_OBSOLETE: return "Obsolete cURL error (50), removed after version 7.7.3";
			case CURLE_SSL_PEER_CERTIFICATE: return "SSL peer's certificate wasn't ok";
			case CURLE_GOT_NOTHING: return "Got nothing";
			case CURLE_SSL_ENGINE_NOTFOUND: return "SSL crypto engine not found";
			case CURLE_SSL_ENGINE_SETFAILED: return "Cannot set SSL crypto engine as default";
			case CURLE_SEND_ERROR: return "Failed sending network data";
			case CURLE_RECV_ERROR: return "Failure in receiving network data";
			case CURLE_SHARE_IN_USE: return "Share is in use";
			case CURLE_SSL_CERTPROBLEM: return "Problem with the local certificate";
			case CURLE_SSL_CIPHER: return "Couldn't use specified cipher";
			case CURLE_SSL_CACERT: return "Problem with the CA cert (path?)";
			case CURLE_BAD_CONTENT_ENCODING: return "Unrecognized transfer encoding";
		}
		char tcMessage[80];
		sprintf(tcMessage, "internal error: unrecognized 'libcURL' error code (%d)", (int) aCode);
		throw UtlException(tcMessage);
	}

	CURL* fillCurlSession(DtaScriptVariable* pSession, CurlOutputData& theCurlOutput) {
		CURL* pCURL = curl_easy_init();
		if (pCURL != 0L) {
			DtaScriptVariable* pAgent = (pSession != 0L) ? pSession->getNode("agent") : 0L;
			const char* tcAgent = ((pAgent != 0L) ? pAgent->getValue() : 0L);
			curl_easy_setopt(pCURL, CURLOPT_USERAGENT, ((tcAgent != 0L) ? tcAgent : "CodeWorker"));
			DtaScriptVariable* pReferer = (pSession != 0L) ? pSession->getNode("referer") : 0L;
			if (pReferer != 0L) {
				const char* tcReferer = pReferer->getValue();
				curl_easy_setopt(pCURL, CURLOPT_REFERER, ((tcReferer == 0L) ? "" : tcReferer));
			}
			DtaScriptVariable* pProxy = (pSession != 0L) ? pSession->getNode("proxy") : 0L;
			if (pProxy != 0L) {
				DtaScriptVariable* pHost = pProxy->getNode("host");
				DtaScriptVariable* pPort = pProxy->getNode("port");
				if ((pHost == 0L) || (pPort == 0L)) {
					throw UtlException("'proxy.host' and 'proxy.port' expected into the HTTP session");
				}
				const char* tcHost = pHost->getValue();
				if (tcHost == 0L) {
					throw UtlException("non empty 'proxy.host' expected into the HTTP session");
				}
				curl_easy_setopt(pCURL, CURLOPT_PROXY, tcHost);
				curl_easy_setopt(pCURL, CURLOPT_PROXYPORT, pPort->getIntValue());
				DtaScriptVariable* pUser = pProxy->getNode("userpwd");
				if (pUser != 0L) {
					const char* tcUserPwd = pUser->getValue();
					if (strchr(tcUserPwd, ':') == 0L) {
						throw UtlException("'proxy.userpwd' must write the user and the password like 'user:pwd'");
					}
					curl_easy_setopt(pCURL, CURLOPT_PROXYUSERPWD, tcUserPwd);
				}
			}
			DtaScriptVariable* pCookies = (pSession != 0L) ? pSession->getNode("cookies") : 0L;
			if (pCookies != 0L) {
				std::string sCookies;
				const std::list<DtaScriptVariable*>* cookies = pCookies->getArray();
				if (cookies != 0L) {
					for (std::list<DtaScriptVariable*>::const_iterator i = cookies->begin(); i != cookies->end(); i++) {
						DtaScriptVariable* pName = (*i)->getNode("name");
						if (pName == 0L) throw UtlException("attribute 'name' of a cookie expected into the HTTP session");
						const char* tcName = pName->getValue();
						if (tcName == 0L) throw UtlException("attribute 'name' of a cookie should be populated into the HTTP session");
						std::string sValue;
						DtaScriptVariable* pValue = (*i)->getNode("value");
						if (pValue != 0L) {
							const char* tcValue = pValue->getValue();
							if (tcValue != 0L) sValue = tcValue;
						}
						if (!sCookies.empty()) sCookies += " ";
						sCookies += std::string(tcName) + "=" + sValue + ";";
					}
				}
				if (!sCookies.empty()) {
					curl_easy_setopt(pCURL, CURLOPT_COOKIE, sCookies.c_str());
				}
			}
			curl_easy_setopt(pCURL, CURLOPT_FILE, &theCurlOutput);
			curl_easy_setopt(pCURL, CURLOPT_WRITEHEADER, &theCurlOutput);
			curl_easy_setopt(pCURL, CURLOPT_HEADERFUNCTION, writeHeaderLineFunction);
			curl_easy_setopt(pCURL, CURLOPT_WRITEFUNCTION, writeCurlFunction);
		}
		return pCURL;
	}

	void populateCookie(DtaScriptVariable* pSession, const std::string& sCookie) {
		std::string::size_type iIndex = sCookie.find_first_of("=;");
		if (iIndex == std::string::npos) return;
		std::string sName = sCookie.substr(0, iIndex);
		std::string sValue;
		std::string::size_type iNext;
		if (sCookie[iIndex++] == '=') {
			iNext  = sCookie.find(';', iIndex);
			if (iNext == std::string::npos) {
				sValue = sCookie.substr(iIndex);
				iIndex = std::string::npos;
			} else {
				sValue = sCookie.substr(iIndex, iNext - iIndex);
				iIndex = iNext + 1;
			}
		}
		DtaScriptVariable* pCookie = pSession->insertNode("cookies")->getArrayElement(sName);
		if (pCookie == 0L) {
			pCookie = pSession->insertNode("cookies")->addElement(sName);
		}
		pCookie->insertNode("name")->setValue(sName.c_str());
		pCookie->insertNode("value")->setValue(sValue.c_str());
		while (iIndex != std::string::npos) {
			iIndex = sCookie.find_first_not_of(' ', iIndex);
			if (iIndex == std::string::npos) return;
			iNext = sCookie.find_first_of("=;", iIndex);
			if (iNext == std::string::npos) {
				sName = sCookie.substr(iIndex);
				sValue = "";
				iIndex = std::string::npos;
			} else {
				sName = sCookie.substr(iIndex, iNext - iIndex);
				iIndex = iNext + 1;
				iNext = sCookie.find(';', iIndex);
				if (iNext == std::string::npos) {
					sValue = sCookie.substr(iIndex);
					iIndex = std::string::npos;
				} else {
					sValue = sCookie.substr(iIndex, iNext - iIndex);
					iIndex = iNext + 1;
				}
			}
			if (!sName.empty()) {
				pCookie->insertNode(sName.c_str())->setValue(sValue.c_str());
			}
		}
	}

	std::string handleCurlOutput(DtaScriptVariable* pSession, CurlOutputData& theCurlOutput) {
		char* tcArray = theCurlOutput.array();
		int iLength = theCurlOutput.length();
		if (theCurlOutput.isBinary()) {
			pSession->setValueAtVariable("binary_data", "true");
			char* tcResult = new char[iLength * 2];
			char* pResult = tcResult;
			for (int i = 0; i < iLength; i++) {
				unsigned char a = (unsigned char) tcArray[i];
				*pResult = CGRuntime::_tcHexa[a >> 4];
				pResult++;
				*pResult = CGRuntime::_tcHexa[a & '\x0F'];
				pResult++;
			}
			delete [] tcArray;
			tcArray = tcResult;
			iLength += iLength;
		} else {
			pSession->remove("binary_data");
		}
		if (!theCurlOutput.headers.empty()) {
			DtaScriptVariable* pHeaders = pSession->insertNode("header_lines");
			pHeaders->clearArray();
			for (std::map<std::string, std::list<std::string> >::const_iterator i = theCurlOutput.headers.begin(); i != theCurlOutput.headers.end(); ++i) {
				DtaScriptVariable* pLine = pHeaders->addElement(i->first);
				bool bCookie = (i->first == "Set-Cookie");
				for (std::list<std::string>::const_iterator j = i->second.begin(); j != i->second.end(); ++j) {
					pLine->pushItem(*j);
					if (bCookie) populateCookie(pSession, *j);
				}
			}
		} else {
			pSession->remove("header_lines");
		}
		std::string sResult = std::string(tcArray, iLength);
		delete [] tcArray;
		return sResult;
	}

#endif

	std::string HTTPRequest::get(const std::string& sURL, DtaScriptVariable* pSession, DtaScriptVariable* pArguments) {
#ifdef CODEWORKER_CURL_LIB
		CurlOutputData theCurlOutput;
		CURL* pCURL = fillCurlSession(pSession, theCurlOutput);
		if (pCURL != 0L) {
			std::string sGetURL = sURL;
			if (pArguments != 0L) {
				const std::list<DtaScriptVariable*>* arguments = pArguments->getArray();
				if (arguments != 0L) {
					for (std::list<DtaScriptVariable*>::const_iterator i = arguments->begin(); i != arguments->end(); i++) {
						if (i == arguments->begin()) sGetURL += "?";
						else sGetURL += "&";
						char* tcName = curl_escape((*i)->getName(), ::strlen((*i)->getName()));
						sGetURL += tcName;
						const char* tcValue = (*i)->getValue();
						if ((tcValue != 0L) && (*tcValue != '\0')) {
							char* tcGetValue = curl_escape(tcValue, (*i)->getValueLength());
							sGetURL += tcGetValue;
							curl_free(tcGetValue);
						}
						curl_free(tcName);
					}
				}
			}
			curl_easy_setopt(pCURL, CURLOPT_URL, sGetURL.c_str());
			CURLcode res = curl_easy_perform(pCURL);
			curl_easy_cleanup(pCURL);
			std::string sErrorCode = getCurlErrorCode(res);
			if (!sErrorCode.empty()) throw UtlException("libcURL error: " + sErrorCode);
		}
		return handleCurlOutput(pSession, theCurlOutput);
#else
		throw UtlException("HTTPRequest::get(" + sURL + ") cannot work, because 'CodeWorker' doesn't link libCURL!");
#endif
	}

	std::string HTTPRequest::post(const std::string& sURL, DtaScriptVariable* pSession, DtaScriptVariable* pArguments) {
#ifdef CODEWORKER_CURL_LIB
		CurlOutputData theCurlOutput;
		CURL* pCURL = fillCurlSession(pSession, theCurlOutput);
		if (pCURL != 0L) {
			std::string sPostURL;
			if (pArguments != 0L) {
				const std::list<DtaScriptVariable*>* arguments = pArguments->getArray();
				if (arguments != 0L) {
					for (std::list<DtaScriptVariable*>::const_iterator i = arguments->begin(); i != arguments->end(); i++) {
						if (i != arguments->begin()) sPostURL += "&";
						char* tcName = curl_escape((*i)->getName(), ::strlen((*i)->getName()));
						sPostURL += tcName;
						const char* tcValue = (*i)->getValue();
						if ((tcValue != 0L) && (*tcValue != '\0')) {
							char* tcPostValue = curl_escape(tcValue, (*i)->getValueLength());
							sPostURL += tcPostValue;
							curl_free(tcPostValue);
						}
						curl_free(tcName);
					}
				}
			}
			curl_easy_setopt(pCURL, CURLOPT_POSTFIELDS, sPostURL.c_str());
			curl_easy_setopt(pCURL, CURLOPT_URL, sURL.c_str());
			CURLcode res = curl_easy_perform(pCURL);
			curl_easy_cleanup(pCURL);
			std::string sErrorCode = getCurlErrorCode(res);
			if (!sErrorCode.empty()) throw UtlException("libcURL error: " + sErrorCode);
		}
		return handleCurlOutput(pSession, theCurlOutput);
#else
		throw UtlException("HTTPRequest::post(" + sURL + ") cannot work, because 'CodeWorker' doesn't link libCURL!");
#endif
	}

	std::string HTTPRequest::send(const std::string& sURL, DtaScriptVariable* pSession) {
#ifdef CODEWORKER_CURL_LIB
		CurlOutputData theCurlOutput;
		CURL* pCURL = fillCurlSession(pSession, theCurlOutput);
		if (pCURL != 0L) {
			curl_easy_setopt(pCURL, CURLOPT_URL, sURL.c_str());
			CURLcode res = curl_easy_perform(pCURL);
			curl_easy_cleanup(pCURL);
			std::string sErrorCode = getCurlErrorCode(res);
			if (!sErrorCode.empty()) throw UtlException("libcURL error: " + sErrorCode);
		}
		return handleCurlOutput(pSession, theCurlOutput);
#else
		throw UtlException("HTTPRequest::send(" + sURL + ") cannot work, because 'CodeWorker' doesn't link libCURL!");
#endif
	}

	std::string HTTPRequest::UUID() {
#ifdef WIN32
		::UUID aUuid;

		RPC_STATUS aCreateStatus = ::UuidCreate(&aUuid);
		if (aCreateStatus != RPC_S_OK) throw UtlException("Cannot create a unique Windows UUID");
		unsigned char * aStringUuid;
		RPC_STATUS aConversionStatus = ::UuidToString(&aUuid, &aStringUuid);
		if (aConversionStatus != RPC_S_OK) throw UtlException("Cannot convert a unique Windows UUID to a string");
		std::string ssUuid((char *)aStringUuid); 
		RPC_STATUS aDeallocationStatus = ::RpcStringFree(&aStringUuid);
		if (aDeallocationStatus != RPC_S_OK) throw UtlException("Cannot deallocate UUID memory");
		return ssUuid;
#else
		throw UtlException("UUID() not implemented yet. Use 'uuid.h' and 'libuuid.so' under Linux");
#endif
	}
}
