/**
 * The main entry point for the Red Orchestra 2 WebAdmin. This manages
 * the initial web page request and authentication and session handling.
 * The eventual processing of the request will be done by query handlers.
 *
 * Copyright 2008 Epic Games, Inc. All Rights Reserved
 * Copyright (C) 2011,2014 Tripwire Interactive LLC
 *
 * @author  Michiel 'elmuerte' Hendriks
 */
class WebAdmin extends WebApplication dependsOn(IQueryHandler) config(WebAdmin);

`include(WebAdmin.uci)

/**
 * The menu handler
 */
var WebAdminMenu menu;

/**
 * The authorization handler instance
 */
var IWebAdminAuth auth;

/**
 * Defines the authentication handler class to use instead of the default one.
 */
var globalconfig string AuthenticationClass;

/**
 * The default authentication class
 */
var class/*<IWebAdminAuth>*/ defaultAuthClass;

/**
 * The session handler
 */
var ISessionHandler sessions;

/**
 * The session handler to use instead of the default session handler
 */
var globalconfig string SessionHandlerClass;

/**
 * The default session handler class
 */
var class/*<ISessionHandler>*/ defaultSessClass;

/**
 * The loaded handlers.
 */
var array<IQueryHandler> handlers;

/**
 * The list of query handlers to automativally load
 */
var globalconfig array<string> QueryHandlers;

/**
 * If set to true, use HTTP Basic authentication rather than a HTML form. Using
 * HTTP authentication gives the functionality of automatic re-authentication.
 */
var globalconfig bool bHttpAuth;

/**
 * The starting page. Defaults to /current
 */
var globalconfig string startpage;

/**
 * local storage. Used to construct the auth URLs.
 */
var protected string serverIp;

`if(`isdefined(COOKIE_PREFIX))
/**
 * Prefix used in cookie names to make them safer for multiple servers on the
 * same machine.
 */
var string cookiePrefix;
`endif

/**
 * DataStoreCache class to use, should be a subclass of DataStoreCache
 */
var globalconfig string DataStoreCacheClass;

/**
 * Default datastorecache class to use
 */
var class/*<DataStoreCache>*/ defaultDataStoreCacheClass;

/**
 * Cached datastore values
 */
var DataStoreCache dataStoreCache;

/**
 * If true start the chatlogging functionality
 */
var globalconfig bool bChatLog;

/**
 * A hack to cleanup the stale PlayerController instances which are not being
 * garbage collected but stay around due to the streaming level loading.
 */
var PCCleanUp pccleanup;

/**
 * Used to keep track of config file updated to make sure certain changes are
 * made. The dedicated server doesn't automatically merge updated config files.
 */
var globalconfig int cfgver;

/**
 * If true pages will be served as application/xhtml+xml when the browser
 * supports it, and when the QH claims it supports it.
 */
var globalconfig bool bUseStrictContentType;

var array<WebAdminSkin> Skins;

var string SkinData;

/**
 * Number of octets in the IPv4 to validate for the session. for example, a
 * value of 3 allows the IP to be between x.y.z.0-x.y.z.255 . A value of 0
 * disables validation. Values higher than 4 are useless (because of IPv4).
 */
var globalconfig int sessionOctetValidation;


//!localization
var localized string menuLogout, menuLogoutDesc, AccessDenied, msgNoPrivs,
	msgNoStartPage, msgLogoutNotice, msgUnableToLogout, error404, msgNotFound,
	msgSessionCreateFail, msgWrongAuthCookie, error403, error401, pageLogin,
	pageLoginDesc, pageAboutTitle, pageAboutDesc, msgUnknownDataType, msgInvalidToken,
	msgMaxLoginTries;

/**
 * Defines a subdirectory who's files will override the standard files. Used for
 * localized files.
 */
var localized string HTMLSubDirectory;

/**
 * Used to hash passwords
 */
var HashLib hashLib;

struct FailedAuthRecord
{
	var string ip;
	var int count;
	var string lastUpdate;
};

var array<FailedAuthRecord> authFails;

var globalconfig int MaxAuthFails;

function init()
{
	local class/*<IWebAdminAuth>*/ authClass;
	local class/*<ISessionHandler>*/ sessClass;
	local class/*<DataStoreCache>*/ dscClass;
	local class<Actor> aclass;
	local IpAddr ipaddress;
	local int i;
	local bool doSaveConfig;

    `Log("Starting Killing Floor 2 WebAdmin...",,'WebAdmin');

    doSaveConfig = false;

    CleanupMsgSpecs();

    `if(`WITH_WEBCONX_FIX)
	WebServer.AcceptClass = class'WebConnectionEx';
    `endif

	if (class'WebConnection'.default.MaxValueLength < 4096)
	{
		class'WebConnection'.default.MaxValueLength = 4096;
		class'WebConnection'.static.StaticSaveConfig();
	}

	super.init();

	if (QueryHandlers.length == 0)
	{
		QueryHandlers[0] = class.getPackageName()$".QHCurrentKF";
		QueryHandlers[1] = class.getPackageName()$".QHDefaultsKF";
		QueryHandlers[3] = class.getPackageName()$".WebAdminSystemSettings";
		doSaveConfig = true;
	}

	if (MaxAuthFails == 0)
	{
		MaxAuthFails = 5;
	}

	if (doSaveConfig)
	{
		SaveConfig();
	}

	if (len(DataStoreCacheClass) != 0)
	{
		dscClass = class(DynamicLoadObject(DataStoreCacheClass, class'Class'));
	}
	if (dscClass == none)
	{
		dscClass = defaultDataStoreCacheClass;
	}
	dataStoreCache = DataStoreCache(new(Self) dscClass);

	menu = new(Self) class'WebAdminMenu';
	menu.webadmin = self;
	menu.addMenu("/about", "", none,, MaxInt-1);
	menu.addMenu("/data", "", none,, MaxInt-1);
	menu.addMenu("/logout", menuLogout, none, menuLogoutDesc, MaxInt);

	if (len(AuthenticationClass) != 0)
	{
		authClass = class(DynamicLoadObject(AuthenticationClass, class'Class'));
	}
	if (authClass == none)
	{
		authClass = defaultAuthClass;
	}

	`Log("Creating IWebAdminAuth instance from: "$authClass,,'WebAdmin');
	if (!ClassIsChildOf(authClass, class'Actor'))
	{
		auth = new(self) authClass;
	}
	else {
		aclass = class<Actor>(DynamicLoadObject(""$authClass, class'Class'));
		auth = Worldinfo.spawn(aclass);
	}
	auth.init(Worldinfo);

    hashLib = new class'Sha1HashLib';
    if (!auth.supportHashAlgorithm(hashLib.getAlgName()))
	{
		`Log(""$authClass$" does not support hash algorithm "$hashLib.getAlgName(),,'WebAdmin');
		hashLib = none;
    }

	if (len(SessionHandlerClass) != 0)
	{
		sessClass = class(DynamicLoadObject(SessionHandlerClass, class'class'));
	}
	if (sessClass == none)
	{
		sessClass = defaultSessClass;
	}

	`Log("Creating ISessionHandler instance from: "$sessClass,,'WebAdmin');
	if (!ClassIsChildOf(sessClass, class'Actor'))
	{
		sessions = new(self) sessClass;
	}
	else {
		aclass = class<Actor>(DynamicLoadObject(""$sessClass, class'Class'));
		sessions = Worldinfo.spawn(aclass);
	}

	WebServer.GetLocalIP(ipaddress);
	serverIp = WebServer.IpAddrToString(ipaddress);
	i = InStr(serverIp, ":");
	if (i > INDEX_NONE)
	{
		serverIp = left(serverIp, i);
	}

	`if(`isdefined(COOKIE_PREFIX))
	cookiePrefix = "_"$worldinfo.Game.GetServerPort()$"_";
	`endif

	initQueryHandlers();
}

function loadWebAdminSkins()
{
//FIXME
/*	local int i;
	local array<ROUIResourceDataProvider> ProviderList;

	class'ROUIDataStore_MenuItems'.static.GetAllResourceDataProviders(class'WebAdminSkin', ProviderList);
	for (i = 0; i < ProviderList.length; i++)
	{
		Skins[i] = WebAdminSkin(ProviderList[i]);
	}*/
}

function CreateChatLog()
{
	if (bChatLog)
	{
		WorldInfo.Spawn(class'ChatLog');
	}
}

function CleanupMsgSpecs()
{
	WorldInfo.Spawn(class'PCCleanUp');
}

/**
 * Clean up the webapplication and everything associated with it.
 */
function CleanupApp()
{
	local IQueryHandler handler;
	foreach handlers(handler)
	{
		handler.cleanup();
	}
	handlers.Remove(0, handlers.Length);
	menu.menu.Remove(0, menu.menu.length);
	menu = none;
	auth.cleanup();
	auth = none;
	sessions.destroyAll();
	sessions = none;
	dataStoreCache.cleanup();
	dataStoreCache = none;
	super.CleanupApp();
}

/**
 * Load the registered query handlers
 */
protected function initQueryHandlers()
{
	local IQueryHandler qh;
	local string entry;
	local class/*<IQueryHandler>*/ qhc;
	local class<Actor> aclass;

	foreach QueryHandlers(entry)
	{
		qhc = class(DynamicLoadObject(entry, class'class'));
		if (qhc == none)
		{
			`Log("Unable to find query handler class: "$entry,,'WebAdmin');
			continue;
		}
		qh = none;
		if (!ClassIsChildOf(qhc, class'Actor'))
		{
			qh = new(self) qhc;
		}
		else {
			aclass = class<Actor>(DynamicLoadObject(""$qhc, class'Class'));
			qh = Worldinfo.spawn(aclass);
		}
		if (qh == none)
		{
			`Log("Unable to create query handler: "$entry,,'WebAdmin');
		}
		else {
			addQueryHandler(qh);
		}
	}
}

/**
 * Add a query handler to the list. This will also call init() and
 * registerMenuItems() on the query handler.
 */
function addQueryHandler(IQueryHandler qh)
{
	if (handlers.find(qh) != INDEX_NONE)
	{
		return;
	}
	qh.init(self);
	qh.registerMenuItems(menu);
	handlers.addItem(qh);
}

/**
 * return the authentication URL string used in the user privileged system.
 */
function string getAuthURL(string forpath)
{
	if (Left(forpath, 1) != "/") forpath = "/"$forpath;
	return "webadmin://"$ serverIp $":"$ WebServer.CurrentListenPort $ forpath;
}

/**
 * Main entry point for the webadmin
 */
function Query(WebRequest Request, WebResponse Response)
{
	local WebAdminQuery currentQuery;
	local WebAdminMenu wamenu;
	local IQueryHandler handler;
	local string title, description;
	local bool acceptsXhtmlXml;
	local int i;

	response.Subst("webadmin.path", path);
	response.Subst("page.uri", Request.URI);
	response.Subst("page.fulluri", Path$Request.URI);
	response.Subst("random", Rand(MaxInt));

	if (len(SkinData) == 0)
	{
		if (skins.length == 0)
		{
			loadWebAdminSkins();
		}
		for (i = 0; i < Skins.length; i++)
		{
			response.Subst("webadminskin.name", `HTMLEscape(Skins[i].name));
			response.Subst("webadminskin.friendlyname", `HTMLEscape(Skins[i].FriendlyName));
			response.Subst("webadminskin.cssfile", `HTMLEscape(Skins[i].cssfile));
			SkinData $= response.LoadParsedUHTM(Path $ "/webadminskin_meta.inc");
		}
		if (skins.length == 0)
		{
			SkinData $= " ";
		}
	}
	response.Subst("webadminskins.meta", SkinData);

	if (InStr(Request.GetHeader("accept-encoding")$",", "gzip,") != INDEX_NONE)
	{
		if (InStr(Request.GetHeader("user-agent"), "Safari/") != INDEX_NONE)
		{
			// Safari lies, it doesn't support gzip encoded files
			response.Subst("client.gzip", "");
		}
		else if (InStr(Request.GetHeader("user-agent"), "MSIE 6.") != INDEX_NONE)
		{
			// MSIE 6. has issues with gzip
			response.Subst("client.gzip", "");
		}
		else {
			response.Subst("client.gzip", ".gz");
		}
	}
	else {
		response.Subst("client.gzip", "");
	}

	if (InStr(Request.GetHeader("accept"), "application/xhtml+xml") != INDEX_NONE)
	{
		acceptsXhtmlXml = bUseStrictContentType;
	}

	if (WorldInfo.IsInSeamlessTravel())
	{
		if (acceptsXhtmlXml) response.AddHeader("Content-Type: application/xhtml+xml");
		response.HTTPResponse("HTTP/1.1 503 Service Unavailable");
		response.subst("html.headers", "<meta http-equiv=\"refresh\" content=\"10\"/>");
		response.IncludeUHTM(Path $ "/servertravel.html");
		response.ClearSubst();
		return;
	}

	currentQuery.request = Request;
	currentQuery.response = Response;
	parseCookies(Request.GetHeader("cookie", ""), currentQuery.cookies);

	if (!getSession(currentQuery))
	{
		return;
	}

	if (len(pageAboutTitle) == 0)
	{
		addMessage(currentQuery, "No localization data. Please make sure the file Localization/INT/WebAdmin.int is up to date.", MT_Error);
	}

	if (!getWebAdminUser(currentQuery))
	{
		return;
	}
	response.Subst("admin.name", currentQuery.user.getUsername());

	wamenu = WebAdminMenu(currentQuery.session.getObject("WebAdminMenu"));
	if (wamenu == none)
	{
		wamenu = menu.getUserMenu(currentQuery.user);
		if (wamenu != none)
		{
			currentQuery.session.putObject("WebAdminMenu", wamenu);
			currentQuery.session.putString("WebAdminMenu.rendered", wamenu.render());
		}
	}
	if (wamenu == none)
	{
		Response.HTTPResponse("HTTP/1.1 403 Forbidden");
		pageGenericError(currentQuery, msgNoPrivs, AccessDenied);
		return;
	}
	response.Subst("navigation.menu", currentQuery.session.getString("WebAdminMenu.rendered"));

	currentQuery.user.clearCheckedPrivileges();

	if (request.URI == "/")
	{
		if (len(startpage) != 0)
		{
			Response.Redirect(path$startpage);
			return;
		}
		pageGenericError(currentQuery, msgNoStartPage);
		return;
	}
	else if (request.URI == "/logout")
	{
		if (auth.logout(currentQuery.user))
		{
			sessions.destroy(currentQuery.session);
			//response.headers[response.headers.length] = "Set-Cookie: sessionid=; Path="$path$"/; Max-Age=0";
			sendCookie(currentQuery, "sessionid", "", path, 0);
			//response.headers[response.headers.length] = "Set-Cookie: authcred=; Path="$path$"/; Max-Age=0";
			sendCookie(currentQuery, "authcred", "", path, 0);
			//response.headers[response.headers.length] = "Set-Cookie: authtimeout=; Path="$path$"/; Max-Age=0";
			sendCookie(currentQuery, "authtimeout", "", path, 0);
			if (bHttpAuth)
			{
				response.Subst("navigation.menu", "");
				//response.headers[response.headers.length] = "Set-Cookie: forceAuthentication=1; Path="$path$"/";
				sendCookie(currentQuery, "forceAuthentication", "1", path);
				addMessage(currentQuery, msgLogoutNotice, MT_Warning);
				pageGenericInfo(currentQuery, "");
				return;
			}
			Response.Redirect(path$"/");
			return;
		}
		pageGenericError(currentQuery, msgUnableToLogout);
		return;
	}
	else if (request.URI == "/about")
	{
		if (acceptsXhtmlXml) response.AddHeader("Content-Type: application/xhtml+xml");
		pageAbout(currentQuery);
		return;
	}
	else if (request.URI == "/data")
	{
		pageData(currentQuery);
		return;
	}

	// get proper handler
	handler = wamenu.getHandlerFor(request.URI, title, description);
	if (handler != none)
	{
		if (acceptsXhtmlXml && handler.producesXhtml()) response.AddHeader("Content-Type: application/xhtml+xml");
		response.Subst("page.title", title);
		response.Subst("page.description", description);
		if (handler.handleQuery(currentQuery))
		{
			return;
		}
	}

	if (currentQuery.user.canPerform(getAuthURL(request.URI))) {
		// try other way
		foreach handlers(handler)
		{
			if (handler.unhandledQuery(currentQuery))
			{
				return;
			}
		}
	}

	// check with the overal menu, if the handler is null the page doesn't exist
	if (acceptsXhtmlXml) response.AddHeader("Content-Type: application/xhtml+xml");
	if (menu.getHandlerFor(request.URI, title, description) == none)
	{
		Response.HTTPResponse("HTTP/1.1 404 Not Found");
		pageGenericError(currentQuery, msgNotFound, error404);
	}
	else {
		Response.HTTPResponse("HTTP/1.1 403 Forbidden");
		pageGenericError(currentQuery, msgNoPrivs, AccessDenied);
	}
}

/**
 * Parse the cookie HTTP header
 */
protected function parseCookies(String cookiehdr, out array<KeyValuePair> cookies)
{
	local array<string> cookieParts;
	local string entry;
	local int pos;
	local KeyValuePair kvp;

	`if(`isdefined(COOKIE_PREFIX))
	local string prefix;
	local int pfpos;
	`endif

	ParseStringIntoArray(cookiehdr, cookieParts, ";", true);
	foreach cookieParts(entry)
	{
		pos = InStr(entry, "=");
		if (pos > INDEX_NONE)
		{
			kvp.key = Left(entry, pos);
			kvp.key -= " ";
			`if(`isdefined(COOKIE_PREFIX))
			if (left(kvp.key, 1) == "%")
			{
				// check prefix
				pfpos = InStr(kvp.key, "_");
				if (pfpos != INDEX_NONE)
				{
					prefix = mid(kvp.key, 1, pfpos-1);
					if (prefix == string(int(prefix)))
					{
						if (left(kvp.key, len(cookiePrefix)) != cookiePrefix)
						{
							continue;
						}
						kvp.key = mid(kvp.key, len(cookiePrefix));
						pfpos = cookies.Find('key', kvp.key);
						if (pfpos != INDEX_NONE)
						{
							cookies.remove(pfpos, 1);
						}
					}
				}
			}
			`endif
			kvp.value = Mid(entry, pos+1);
			if (left(kvp.value, 1) == "\"")
            {
                // unquote
                kvp.value = repl(mid(kvp.value, 1, len(kvp.value) - 2), "\\\"", "\"");
            }
			//`Log("Received cookie with name="$kvp.key$" ; value="$kvp.value,,'WebAdmin');
			cookies.AddItem(kvp);
		}
	}
}

/**
 * Send a cookie to the client. Returns true when the cookie was included in
 * the output. If the headers where already send false will be returned.
 */
function bool sendCookie(out WebAdminQuery q, string key, coerce string value,
	optional string cpath = "", optional int maxage = -1, optional string domain = "")
{
	local string cookie;
	if (q.response.SentText()) return false;
	key = `trim(key);
	if (len(key) == 0) return false;
	`if(`isdefined(COOKIE_PREFIX))
	key = cookiePrefix$key; // add prefix
	`endif
	cookie = "Set-Cookie: "$key$"=\""$repl(value, "\"", "\\\"")$"\"";
	if (len(cpath) > 0)
	{
		if (right(cpath, 1) != "/") cpath $= "/";
		cookie $= "; path="$cpath;
	}
	if (len(domain) > 0)
	{
		cookie $= "; domain="$domain;
	}
	if (maxage > -1)
	{
		cookie $= "; expires=\""$class'WebAdminUtils'.static.convertToRfc2109Date(q.response.GetHTTPExpiration(maxage))$"\"";
		cookie $= "; max-age="$maxage;
	}
	q.response.headers[q.response.headers.length] = cookie;
	//`log(cookie,,'WebAdmin');
	return true;
}

/**
 * Adds the ISession instance to query
 */
protected function bool getSession(out WebAdminQuery q)
{
	local string sessionId;
	local int idx;

	idx = q.cookies.Find('key', "sessionid");
	if (idx > INDEX_NONE)
	{
		sessionId = q.cookies[idx].value;
	}
	if (len(sessionId) == 0)
	{
		sessionId = q.request.GetVariable("sessionid");
	}
	if (len(sessionId) > 0)
	{
		q.session = sessions.get(sessionId);
		if (q.session != none && sessionOctetValidation > 0)
		{
			validateSessionOctet(q);
		}
	}
	if (q.session == none)
	{
		q.session = sessions.create();
		idx = inStr(q.request.RemoteAddr, ":");
		if (idx == INDEX_NONE) idx = len(q.request.RemoteAddr);
		q.session.putString("AuthIP", Left(q.request.RemoteAddr, idx));
		//q.response.headers[q.response.headers.length] = "Set-Cookie: sessionid="$q.session.getId()$"; Path="$path$"/";
		sendCookie(q, "sessionid", q.session.getId(), path);
	}
	if (q.session == none)
	{
		pageGenericError(q, msgSessionCreateFail);
		return false;
	}
	q.response.Subst("sessionid", q.session.getId());
	return true;
}

protected function validateSessionOctet(out WebAdminQuery q)
{
	local array<string> ip1, ip2;
	local int i;
	i = inStr(q.request.RemoteAddr, ":");
	if (i == INDEX_NONE) i = len(q.request.RemoteAddr);
	ParseStringIntoArray(Left(q.request.RemoteAddr, i), ip1, ".", false);
	ParseStringIntoArray(q.session.getString("AuthIP", "0.0.0.0"), ip2, ".", false);
	ip1.length = sessionOctetValidation;
	ip2.length = sessionOctetValidation;
	for (i = 0; i < ip1.length; ++i)
	{
		if (int(ip1[i]) != int(ip2[i]))
		{
			q.session = none;
			break;
		}
	}
}

/**
 * Retreives the webadmin user. Creates a new one when needed.
 */
protected function bool getWebAdminUser(out WebAdminQuery q)
{
	local string username, password, token, errorMsg, rememberCookie, hashAlgName;
	local int idx;
	local bool checkToken;

	local string realm;
	if (bHttpAuth)
	{
		realm = "RO2 WebAdmin - "$worldinfo.Game.GameReplicationInfo.ServerName;
		q.response.AddHeader("WWW-authenticate: basic realm=\""$realm$"\"");
		q.session.putString("UsedHttpAuth", "1");
	}

	q.user = q.session.getObject("IWebAdminUser");
	// 1: find existing user
	if (q.user != none)
	{
		if (q.session.getString("UsedHttpAuth") == "1")
		{
			// not really needed
			if (!auth.validate(q.request.Username, q.request.Password, "", errorMsg))
			{
				addMessage(q, errorMsg, MT_Error);
				pageAuthentication(q);
				return false;
			}
		}
		else {
			if (q.session.getString("AuthTimeout") == "1")
			{
				if (q.cookies.Find('key', "authcred") == INDEX_NONE)
				{
					q.session.removeString("AuthTimeout");
					q.session.removeObject("IWebAdminUser");
					auth.logout(q.user);
					q.user = none;
					addMessage(q, "Session timeout.", MT_Error);
					pageAuthentication(q);
					return false;
				}
			}
			setAuthCredCookie(q, "", -2);
		}
		return true;
	}

	idx = q.cookies.Find('key', "authcred");
	if (idx != INDEX_NONE)
	{
		rememberCookie = q.cookies[idx].value;
	}
	else {
		rememberCookie = "";
	}

	checkToken = false;

	// 2: try to authenticate
	if (len(q.request.Username) > 0 && len(q.request.Password) > 0)
	{
		username = q.request.Username;
		password = q.request.Password;
		if (bHttpAuth)
		{
			idx = q.cookies.Find('key', "forceAuthentication");
			if (idx != INDEX_NONE && q.cookies[idx].value == "1")
			{
				//q.response.headers[q.response.headers.length] = "Set-Cookie: forceAuthentication=; Path="$path$"/; Max-Age=0";
				sendCookie(q, "forceAuthentication", "", path, 0);
				pageAuthentication(q);
				return false;
			}
		}
	}
	else if (len(rememberCookie) > 0)
	{
		username = q.request.DecodeBase64(rememberCookie);
		idx = InStr(username, Chr(10));
		if (idx != INDEX_NONE)
		{
			password = Mid(username, idx+1);
			username = Left(username, idx);
		}
		else {
			username = "";
		}
	}

	// not set, check request variables
	if (len(username) == 0 || len(password) == 0)
	{
		username = q.request.GetVariable("username");
		password = q.request.GetVariable("password_hash");
		if (len(password) == 0) password = q.request.GetVariable("password");
		token = q.request.GetVariable("token");
		checkToken = true;
	}

	// request authentication
	if (len(username) == 0 || len(password) == 0)
	{
		pageAuthentication(q);
		return false;
	}

	// check data
	if (checkToken && (len(token) == 0 || token != q.session.getString("AuthFormToken")))
	{
		addMessage(q, msgInvalidToken, MT_Error);
		pageAuthentication(q);
		return false;
	}

	if (Left(password, 1) == "$")
	{
		idx = InStr(password, "$",,, 1);
		if (idx != INDEX_NONE)
		{
			hashAlgName = mid(password, 1, idx-1);
			password = mid(password, idx+1);
		}
	}

	if (exceededAuthFail(q, username))
	{
		q.response.HTTPResponse("HTTP/1.1 403 Forbidden");
		pageGenericError(q, msgMaxLoginTries);
		return false;
	}

	q.user = auth.authenticate(username, password, hashAlgName, errorMsg);

	if (q.user == none)
	{
		recordAuthFail(q, username);

		addMessage(q, errorMsg, MT_Error);
		if (len(rememberCookie) > 0)
		{
			// unset cookie
			//q.response.headers[q.response.headers.length] = "Set-Cookie: authcred=; Path="$path$"/; Max-Age=0";
			sendCookie(q, "authcred", "", path, 0);
			//q.response.headers[q.response.headers.length] = "Set-Cookie: authtimeout=; Path="$path$"/; Max-Age=0";
			sendCookie(q, "authtimeout", "", path, 0);
			addMessage(q, msgWrongAuthCookie, MT_Error);
			rememberCookie = "";
		}
		pageAuthentication(q);
		return false;
	}
	q.session.putObject("IWebAdminUser", q.user);
	resetAuthFail(q, username);

	if (q.request.GetVariable("remember") != "")
	{
		if (hashLib != none)
		{
			if (hashAlgName == "")
			{
				// received password wasn't hashed
				password = hashLib.getHash(password$username);
			}
			password = "$"$hashLib.getAlgName()$"$"$password;
			rememberCookie = q.request.EncodeBase64(username$chr(10)$password);
		}
		else {
			rememberCookie = q.request.EncodeBase64(username$chr(10)$password);
		}
		setAuthCredCookie(q, rememberCookie, int(q.request.GetVariable("remember")));
	}

	return true;
}

function bool exceededAuthFail(out WebAdminQuery q, string username)
{
	local int idx;
	idx = authFails.find('ip', q.request.RemoteAddr);
	if (idx != INDEX_NONE)
	{
		if (authFails[idx].count > MaxAuthFails)
		{
			return true;
		}
	}
	return false;
}

function recordAuthFail(out WebAdminQuery q, string username)
{
	local int idx;
	idx = authFails.find('ip', q.request.RemoteAddr);
	if (idx == INDEX_NONE)
	{
		idx = authFails.length;
		authFails.length = idx+1;
		authFails[idx].count = 0;
		authFails[idx].ip = q.request.RemoteAddr;
	}
	authFails[idx].count = authFails[idx].count + 1;
	authFails[idx].lastUpdate = timestamp();
}

function resetAuthFail(out WebAdminQuery q, string username)
{
	local int idx;
	idx = authFails.find('ip', q.request.RemoteAddr);
	if (idx != INDEX_NONE)
	{
		authFails.remove(idx, 1);
	}
}

/**
 * Set the cookie data to remember the current authetication attempt
 */
function setAuthCredCookie(out WebAdminQuery q, string creddata, int timeout)
{
	local int idx;
	if (timeout == -2)
	{
		idx = q.cookies.Find('key', "authtimeout");
		if (idx != INDEX_NONE)
		{
			timeout = int(q.cookies[idx].value);
		}
		else {
			timeout = 0;
		}
	}
	if (len(creddata) == 0)
	{
		idx = q.cookies.Find('key', "authcred");
		if (idx != INDEX_NONE)
		{
			creddata = q.cookies[idx].value;
		}
	}
	if (len(creddata) == 0)
	{
		return;
	}
	if (timeout > 0)
	{
		//q.response.headers[q.response.headers.length] = "Set-Cookie: authcred="$creddata$"; Path="$path$"/; Max-Age="$timeout;
		sendCookie(q, "authcred", creddata, path, timeout);
		//q.response.headers[q.response.headers.length] = "Set-Cookie: authtimeout="$timeout$"; Path="$path$"/; Max-Age="$timeout;
		sendCookie(q, "authtimeout", timeout, path, timeout);
		q.session.putString("AuthTimeout", "1");
	}
	else if (timeout == -1)
	{
		//q.response.headers[q.response.headers.length] = "Set-Cookie: authcred="$creddata$"; Path="$path$"/";
		sendCookie(q, "authcred", creddata, path);
	}
	// else don't remember
}

/**
 * Get the messages stored for the current user.
 */
function WebAdminMessages getMessagesObject(WebAdminQuery q)
{
	local WebAdminMessages msgs;
	msgs = WebAdminMessages(q.session.getObject("WebAdmin.Messages"));
	if (msgs == none)
	{
		msgs = new class'WebAdminMessages';
		q.session.putObject("WebAdmin.Messages", msgs);
	}
	return msgs;
}

/**
 * Add a certain message. These messages will be processed at a later stage.
 */
function addMessage(WebAdminQuery q, string msg, optional EMessageType type = MT_Information)
{
	local WebAdminMessages msgs;
	if (len(msg) == 0) return;
	msgs = getMessagesObject(q);
	msgs.addMessage(msg, type);
}

/**
 * Render the message structure to HTML.
 */
function string renderMessages(WebAdminQuery q)
{
	local WebAdminMessages msgs;
	msgs = WebAdminMessages(q.session.getObject("WebAdmin.Messages"));
	if (msgs == none) return "";
	return msgs.renderMessages(self, q);
}

function string renderPrivilegeLog(WebAdminQuery q)
{
	local array<string> privs;
	local int i, j;
	local string tmp, entry;

	privs = q.user.getCheckedPrivileges();
	tmp = "";
	privs.InsertItem(0, getAuthURL(q.request.uri));
	for (i = 0; i < privs.length; ++i)
	{
		entry = privs[i];
		if (left(entry, 11) != "webadmin://")
		{
			continue;
		}
		j = InStr(entry, "/",,,11);
		if (j == INDEX_NONE)
		{
			continue;
		}
		entry = Mid(entry, j);
		q.response.Subst("privilege.log.entry", entry);
		tmp $= include(q, "privilege_log_entry.inc");
	}
	q.response.Subst("privilege.log", tmp);
	return include(q, "privilege_log.inc");
}

/**
 * Include the specified file.
 */
function string include(WebAdminQuery q, string file)
{
	if ((len(HTMLSubDirectory) > 0) && q.response.FileExists(Path $ "/" $ HTMLSubDirectory $ "/" $ file))
	{
		return q.response.LoadParsedUHTM(Path $ "/" $ HTMLSubDirectory $ "/" $ file);
	}
	return q.response.LoadParsedUHTM(Path $ "/" $ file);
}

function bool hasIncludeFile(WebAdminQuery q, string file)
{
	if ((len(HTMLSubDirectory) > 0) && q.response.FileExists(Path $ "/" $ HTMLSubDirectory $ "/" $ file))
	{
		return true;
	}
	return q.response.FileExists(Path $ "/" $ file);
}

/**
 * Load the given file and send it to the client.
 */
function sendPage(WebAdminQuery q, string file)
{
	local IQueryHandler handler;
	foreach handlers(handler)
	{
		handler.decoratePage(q);
	}
	q.response.Subst("messages", renderMessages(q));
	if (q.session.getString("privilege.log") != "")
	{
		q.response.Subst("privilege.log", renderPrivilegeLog(q));
	}

	if ((len(HTMLSubDirectory) > 0) && q.response.FileExists(Path $ "/" $ HTMLSubDirectory $ "/" $ file))
	{
		q.response.IncludeUHTM(Path $ "/" $ HTMLSubDirectory $ "/" $ file);
	}
	else {
		q.response.IncludeUHTM(Path $ "/" $ file);
	}
	q.response.ClearSubst();
}

/**
 * Create a generic error message.
 */
function pageGenericError(WebAdminQuery q, coerce string errorMsg, optional string title = "Error")
{
	if (q.acceptsXhtmlXml) q.response.AddHeader("Content-Type: application/xhtml+xml");
	q.response.Subst("page.title", title);
	q.response.Subst("page.description", "");
	addMessage(q, errorMsg, MT_Error);
	sendPage(q, "message.html");
}

/**
 * Create a generic information message.
 */
function pageGenericInfo(WebAdminQuery q, coerce string msg, optional string title = "Information")
{
	if (q.acceptsXhtmlXml) q.response.AddHeader("Content-Type: application/xhtml+xml");
	q.response.Subst("page.title", title);
	q.response.Subst("page.description", "");
	addMessage(q, msg);
	sendPage(q, "message.html");
}

/**
 * Produces the authentication page.
 */
function pageAuthentication(WebAdminQuery q)
{
	local string token;
	if (q.request.getVariable("ajax") == "1")
	{
		q.response.HTTPResponse("HTTP/1.1 403 Forbidden");
		pageGenericError(q, msgNoPrivs, error403);
		return;
	}
	if (bHttpAuth)
	{
		q.response.HTTPResponse("HTTP/1.1 401 Unauthorized");
		pageGenericError(q, msgNoPrivs, error401);
		return;
	}
	if (q.acceptsXhtmlXml) q.response.AddHeader("Content-Type: application/xhtml+xml");
	token = Right(ToHex(Rand(0xFFFF)), 4)$Right(ToHex(Rand(0xFFFF)), 4);
	q.session.putString("AuthFormToken", token);
	q.response.Subst("page.title", pageLogin);
	q.response.Subst("page.description", pageLoginDesc);
	q.response.Subst("token", token);
	if (hashLib == none)
	{
		q.response.Subst("hashalg", "");
	}
	else {
		q.response.Subst("hashalg", hashLib.getAlgName());
	}
	sendPage(q, "login.html");
}

/**
 * Show the about page
 */
function pageAbout(WebAdminQuery q)
{
	local OnlineGameSettings ogs;
	local OnlineSubsystem steamworks;

	q.response.Subst("page.title", pageAboutTitle);
	q.response.Subst("page.description", pageAboutDesc);
	q.response.Subst("engine.version", worldinfo.EngineVersion);
	q.response.Subst("engine.netversion", worldinfo.MinNetVersion);
	q.response.Subst("client.address", q.request.RemoteAddr);
	q.response.Subst("webadmin.address", serverIp$":"$WebServer.CurrentListenPort);
	if (bHttpAuth) q.response.Subst("webadmin.authmethod", "HTTP Authentication");
	else q.response.Subst("webadmin.authmethod", "Login form");
	if (q.cookies.Find('key', "authcred") > INDEX_NONE) q.response.Subst("client.remember", "True");
	else q.response.Subst("client.remember", "False");
	q.response.Subst("client.sessionid", q.session.getId());
	q.response.Subst("client.authip", q.session.getString("AuthIP"));
	q.response.Subst("client.useragent", q.request.GetHeader("user-agent"));


	if (worldinfo.game.GameInterface != none)
	{
		ogs = worldinfo.game.GameInterface.GetGameSettings(worldinfo.game.PlayerReplicationInfoClass.default.SessionName);
	}

	steamworks = class'GameEngine'.static.GetOnlineSubsystem();

	if (ogs != none)
	{
		q.response.subst("player.uniqueid", class'WebAdminUtils'.static.UniqueNetIdToString(ogs.OwningPlayerId));
		if (steamworks != none)
		{
			q.response.subst("player.steamid", `HTMLEscape(steamworks.UniqueNetIdToPlayerName(ogs.OwningPlayerId)));
		}
		else {
			q.response.subst("player.steamid", "");
		}
	}
	else {
		q.response.subst("player.uniqueid", "");
		q.response.subst("player.steamid", "");
	}

	if (steamworks != none)
	{
		q.response.subst("player.status", steamworks.GetLoginStatus(0));
		q.response.subst("engine.nattype", steamworks.GetNATType());
	}
	else {
		q.response.subst("player.status", "");
		q.response.subst("engine.nattype", "");
	}

	sendPage(q, "about.html");
}

/**
 * Generic XML data provider, could be used by AJAX calls.
 */
function pageData(WebAdminQuery q)
{
	local string tmp;
	local int i, j;

	local DCEGameInfo gametype;
	local array<DCEMapInfo> maps;
	local array<MutatorGroup> mutators;

	q.response.AddHeader("Content-Type: text/xml");
	q.response.SendText("<request>");

	tmp = q.request.getVariable("type");
	if (tmp == "gametypes") {
		dataStoreCache.loadGameTypes();
		q.response.SendText("<gametypes>");
		foreach dataStoreCache.gametypes(gametype)
	 	{
	 		q.response.SendText("<gametype>");
	 		q.response.SendText("<class>"$`HTMLEscape(gametype.data.ClassName)$"</class>");
	 		q.response.SendText("<friendlyname>"$`HTMLEscape(gametype.FriendlyName)$"</friendlyname>");
 			q.response.SendText("</gametype>");
	 	}
		q.response.SendText("</gametypes>");
	}
	else if (tmp == "maps") {
		q.response.SendText("<maps>");
		maps = dataStoreCache.getMaps(q.request.getVariable("gametype"));
 		for (i = 0; i < maps.length; i++)
 		{
 			q.response.SendText("<map>");
 			q.response.SendText("<name>"$`HTMLEscape(maps[i].MapName)$"</name>");
 			q.response.SendText("<friendlyname>"$`HTMLEscape(maps[i].FriendlyName)$"</friendlyname>");
 			q.response.SendText("</map>");
 		}
 		q.response.SendText("</maps>");
	}
	else if (tmp == "mutators") {
		mutators = dataStoreCache.getMutators(q.request.getVariable("gametype"));
 		for (i = 0; i < mutators.length; i++)
 		{
 			q.response.SendText("<mutatorGroup>");
			q.response.SendText("<name>"$`HTMLEscape(mutators[i].GroupName)$"</name>");
			q.response.SendText("<mutators>");
			for (j = 0; j < mutators[i].mutators.length; j++)
	 		{
	 			q.response.SendText("<mutator>");
	 			q.response.SendText("<class>"$`HTMLEscape(mutators[i].mutators[j].ClassName)$"</class>");
	 			q.response.SendText("<friendlyname>"$`HTMLEscape(mutators[i].mutators[j].FriendlyName)$"</friendlyname>");
	 			q.response.SendText("</mutator>");
	 		}
			q.response.SendText("</mutators>");
 			q.response.SendText("</mutatorGroup>");
 		}
	}
	else {
		addMessage(q, msgUnknownDataType@tmp, MT_Error);
	}

	q.response.SendText("<messages><![CDATA[");
	q.response.SendText(renderMessages(q));
	q.response.SendText("]]></messages>");
	q.response.SendText("</request>");
}

defaultproperties
{
	defaultAuthClass=class'BasicWebAdminAuth'
	defaultSessClass=class'SessionHandler'
	defaultDataStoreCacheClass=class'DataStoreCacheKF'
}
