// SoftEther VPN Source Code
// Cedar Communication Module
// 
// SoftEther VPN Server, Client and Bridge are free software under GPLv2.
// 
// Copyright (c) 2012-2014 Daiyuu Nobori.
// Copyright (c) 2012-2014 SoftEther VPN Project, University of Tsukuba, Japan.
// Copyright (c) 2012-2014 SoftEther Corporation.
// 
// All Rights Reserved.
// 
// http://www.softether.org/
// 
// Author: Tetsuo Sugiyama
// Comments: Tetsuo Sugiyama, Ph.D.
// 
// 
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// version 2 as published by the Free Software Foundation.
// 
// 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 version 2
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// 
// THE LICENSE AGREEMENT IS ATTACHED ON THE SOURCE-CODE PACKAGE
// AS "LICENSE.TXT" FILE. READ THE TEXT FILE IN ADVANCE TO USE THE SOFTWARE.
// 
// 
// THIS SOFTWARE IS DEVELOPED IN JAPAN, AND DISTRIBUTED FROM JAPAN,
// UNDER JAPANESE LAWS. YOU MUST AGREE IN ADVANCE TO USE, COPY, MODIFY,
// MERGE, PUBLISH, DISTRIBUTE, SUBLICENSE, AND/OR SELL COPIES OF THIS
// SOFTWARE, THAT ANY JURIDICAL DISPUTES WHICH ARE CONCERNED TO THIS
// SOFTWARE OR ITS CONTENTS, AGAINST US (SOFTETHER PROJECT, SOFTETHER
// CORPORATION, DAIYUU NOBORI OR OTHER SUPPLIERS), OR ANY JURIDICAL
// DISPUTES AGAINST US WHICH ARE CAUSED BY ANY KIND OF USING, COPYING,
// MODIFYING, MERGING, PUBLISHING, DISTRIBUTING, SUBLICENSING, AND/OR
// SELLING COPIES OF THIS SOFTWARE SHALL BE REGARDED AS BE CONSTRUED AND
// CONTROLLED BY JAPANESE LAWS, AND YOU MUST FURTHER CONSENT TO
// EXCLUSIVE JURISDICTION AND VENUE IN THE COURTS SITTING IN TOKYO,
// JAPAN. YOU MUST WAIVE ALL DEFENSES OF LACK OF PERSONAL JURISDICTION
// AND FORUM NON CONVENIENS. PROCESS MAY BE SERVED ON EITHER PARTY IN
// THE MANNER AUTHORIZED BY APPLICABLE LAW OR COURT RULE.
// 
// USE ONLY IN JAPAN. DO NOT USE IT IN OTHER COUNTRIES. IMPORTING THIS
// SOFTWARE INTO OTHER COUNTRIES IS AT YOUR OWN RISK. SOME COUNTRIES
// PROHIBIT ENCRYPTED COMMUNICATIONS. USING THIS SOFTWARE IN OTHER
// COUNTRIES MIGHT BE RESTRICTED.
// 
// 
// SOURCE CODE CONTRIBUTION
// ------------------------
// 
// Your contribution to SoftEther VPN Project is much appreciated.
// Please send patches to us through GitHub.
// Read the SoftEther VPN Patch Acceptance Policy in advance:
// http://www.softether.org/5-download/src/9.patch
// 
// 
// DEAR SECURITY EXPERTS
// ---------------------
// 
// If you find a bug or a security vulnerability please kindly inform us
// about the problem immediately so that we can fix the security problem
// to protect a lot of users around the world as soon as possible.
// 
// Our e-mail address for security reports is:
// softether-vpn-security [at] softether.org
// 
// Please note that the above e-mail address is not a technical support
// inquiry address. If you need technical assistance, please visit
// http://www.softether.org/ and ask your question on the users forum.
// 
// Thank you for your cooperation.


// WebUI.c
// Web User Interface module

#include "CedarPch.h"

static WU_CONTEXT *WuNewContext(WEBUI *wu, char *hubname);
static void WuFreeContext(WU_CONTEXT *context);
static WU_WEBPAGE *WuNewUniWebPage(wchar_t *content);
static WU_WEBPAGE *WuNewWebPage(char *content, UINT size, char *filename);
static wchar_t *WuErrorPage(UINT errorcode);
static wchar_t *WuRedirectPage(char *url);
static wchar_t *WuUniReadFile(char *filename);
static void WuUniReplace(wchar_t **buf, wchar_t *from, wchar_t *to);
static void WuUniInsertBefore(wchar_t **buf, wchar_t *insert, wchar_t *before);
static LIST *WuAnalyzeTarget(char *target,char *filename, UINT size);
static void WuFreeStrStrMap(LIST *params);
static void WuEnableTag(wchar_t **buf, wchar_t *keyword);
static char *WuNewSessionKey();
static void WuUniStrReplace(wchar_t **buf, wchar_t *from, char *to);
static wchar_t *WuUniGetTemplate(wchar_t **str, wchar_t *start, wchar_t *end, bool erase);
static void WuUniUintReplace(wchar_t **buf, wchar_t *key, UINT num);
static void WuUniUint64Replace(wchar_t **buf, wchar_t *key, UINT64 num);
static wchar_t *WuUniGetTemplate(wchar_t **str, wchar_t *start, wchar_t *end, bool erase);
static LIST *WuUniMakeTable(wchar_t *def);
static LIST *WuUniMakeTableFromTemplate(wchar_t **str, wchar_t *start, wchar_t *end);
static void WuExpireSessionKey(WEBUI *wu);
static WU_CONTEXT *WuGetContext(LIST *contexts, char *sessionkey);

#define WP_DEFAULT		"/webui/"
#define WP_LOGIN		"/webui/login.cgi"
#define WP_REDIRECT		"/webui/redirect.cgi"
#define WP_ERROR		"/webui/error.cgi"
#define WP_SERVER		"/webui/server.cgi"
#define WP_LISTENER		"/webui/listener.cgi"
#define WP_HUB			"/webui/hub.cgi"
#define WP_USER			"/webui/user.cgi"
#define WP_EDITUSER		"/webui/edituser.cgi"
#define WP_NEWHUB		"/webui/newhub.cgi"
#define WP_LICENSE		"/webui/license.cgi"
#define WP_LOCALBRIDGE	"/webui/localbridge.cgi"
#define WP_SECURENAT	"/webui/securenat.cgi"
#define WP_SESSION		"/webui/session.cgi"

static wchar_t *WpDefault(WEBUI *wu, LIST *params);
static wchar_t *WpLogin(WEBUI *wu, LIST *params);
static wchar_t *WpServer(WEBUI *wu, LIST *params);
static wchar_t *WpListener(WEBUI *wu, LIST *params);
static wchar_t *WpHub(WEBUI *wu, LIST *params);
static wchar_t *WpUser(WEBUI *wu, LIST *params);
static wchar_t *WpEditUser(WEBUI *wu, LIST *params);
static wchar_t *WpNewHub(WEBUI *wu, LIST *params);
static wchar_t *WpLicense(WEBUI *wu, LIST *params);
static wchar_t *WpLocalBridge(WEBUI *wu, LIST *params);
static wchar_t *WpSecureNAT(WEBUI *wu, LIST *params);
static wchar_t *WpSession(WEBUI *wu, LIST *params);

// WebUI page handler table
static STRMAP_ENTRY wu_pages[] = {
	{WP_DEFAULT, WpDefault},
	{WP_LOGIN, WpLogin},
	{WP_SERVER, WpServer},
	{WP_LISTENER, WpListener},
	{WP_HUB, WpHub},
	{WP_USER, WpUser},
	{WP_EDITUSER, WpEditUser},
	{WP_NEWHUB, WpNewHub},
	{WP_LICENSE, WpLicense},
	{WP_LOCALBRIDGE, WpLocalBridge},
	{WP_SECURENAT, WpSecureNAT},
	{WP_SESSION, WpSession},
};

// **** Page handlers

// Redirect the directory access to the login screen
static wchar_t *WpDefault(WEBUI *wu, LIST *params)
{
	return WuRedirectPage(WP_LOGIN);
}

// Login page
static wchar_t *WpLogin(WEBUI *wu, LIST *params)
{
	UINT result;
	char random[20], securepass[SHA1_SIZE];
	char *password = (char*)StrMapSearch(params, "PASS");
	char *hubname = (char*)StrMapSearch(params, "HUB");

	if(password == NULL)
	{
		wchar_t *buf = WuUniReadFile("|"WP_LOGIN);
		WuUniStrReplace(&buf, L"ACTION", WP_LOGIN);
		WuUniStrReplace(&buf, L"HUBNAME", hubname == NULL ? "" : hubname);
		return buf;
	}

	// Administrator authentication
	Rand(random,sizeof(random));
	Hash(securepass, password, StrLen(password), true);
	SecurePassword(securepass, securepass, random);
	result = AdminCheckPassword(wu->Cedar, random, securepass, hubname, false, NULL);

	if(result == ERR_NO_ERROR)
	{
		// Successful login
		char tmp[MAX_SIZE];
		STRMAP_ENTRY *context;

		// Create a new context
		context = Malloc(sizeof(STRMAP_ENTRY));
		context->Name = WuNewSessionKey();
		context->Value = WuNewContext(wu, hubname);
		Add(wu->Contexts, context);

		// Transfer to the server management screen
		Format(tmp, sizeof(tmp), "%s?KEY=%s", WP_SERVER, context->Name);
		return WuRedirectPage(tmp);
	}else{
		// Login failure
		wchar_t *buf = WuUniReadFile("|"WP_LOGIN);
		WuUniStrReplace(&buf, L"ACTION", WP_LOGIN);
		WuUniStrReplace(&buf, L"HUBNAME",hubname == NULL ? "" : hubname);
		WuUniReplace(&buf, L"<!--ERR1-->", GetUniErrorStr(result));
		return buf;
	}
}

// Server management
static wchar_t *WpServer(WEBUI *wu, LIST *params)
{
	char *sessionkey = (char*)StrMapSearch(params, "KEY");
	WU_CONTEXT *context = WuGetContext(wu->Contexts, sessionkey);
	UINT i;
	wchar_t *buf;
	LIST *strmap;

	if(context == NULL)
	{
		return WuRedirectPage(WP_LOGIN);
	}

	buf = WuUniReadFile("|"WP_SERVER);
	strmap = WuUniMakeTableFromTemplate(&buf, L"<!--STRMAP:", L":STRMAP-->");

	// Show the Virtual HUB list
	{
		wchar_t *tmpl = WuUniGetTemplate(&buf, L"<!--HUBS_TMPL:", L":HUBS_TMPL-->", true);
		RPC_ENUM_HUB t;

		t.Hubs = NULL;
		StEnumHub(context->Admin, &t);
		for(i=0; i<t.NumHub; i++)
		{
			wchar_t *tmp = UniCopyStr(tmpl);
			wchar_t lastlogin[MAX_SIZE], lastcomm[MAX_SIZE];
			RPC_ENUM_HUB_ITEM *item = &t.Hubs[i];

			GetDateTimeStr64Uni(lastlogin,sizeof(lastlogin), SystemToLocal64(item->LastLoginTime));
			GetDateTimeStr64Uni(lastcomm, sizeof(lastcomm), SystemToLocal64(item->LastCommTime));
			WuUniStrReplace(&tmp, L"{HUBNAME}", item->HubName);
			WuUniReplace(&tmp, L"{HUBSTATE}", item->Online ? StrMapSearch(strmap, "HUB_ONLINE") : StrMapSearch(strmap, "HUB_OFFLINE"));
			WuUniReplace(&tmp, L"{HUBTYPE}", item->HubType == HUB_TYPE_STANDALONE ? StrMapSearch(strmap, "HUB_STANDALONE") 
				: item->HubType == HUB_TYPE_FARM_DYNAMIC ? StrMapSearch(strmap,"HUB_DYNAMIC") : StrMapSearch(strmap, "HUB_STATIC"));
			WuUniUintReplace(&tmp, L"{HUBUSERS}", item->NumUsers);
			WuUniUintReplace(&tmp, L"{HUBGROUPS}", item->NumGroups);
			WuUniUintReplace(&tmp, L"{HUBSESSIONS}", item->NumSessions);
			WuUniUintReplace(&tmp, L"{HUBMACS}", item->NumMacTables);
			WuUniUintReplace(&tmp, L"{HUBIPS}", item->NumIpTables);
			WuUniUintReplace(&tmp, L"{HUBLOGINS}", item->NumLogin);
			WuUniReplace(&tmp, L"{HUBLASTLOGINDATE}", lastlogin);
			WuUniReplace(&tmp, L"{HUBLASTCOMMDATE}", lastcomm);

			WuUniInsertBefore(&buf, tmp, L"<!--HUBS-->");
			Free(tmp);
		}
		FreeRpcEnumHub(&t);
		Free(tmpl);
	}

	// Show the listener list
	{
		RPC_LISTENER_LIST t;
		wchar_t *tmpl = WuUniGetTemplate(&buf, L"<!--LISTENER_TMPL:", L":LISTENER_TMPL-->", true);
		Zero(&t, sizeof(t));
		StEnumListener(context->Admin, &t);
		for(i=0; i<t.NumPort; i++)
		{			
			wchar_t *tmp = UniCopyStr(tmpl);
			WuUniReplace(&tmp, L"{PORT_STATE}", t.Enables[i] == false ? StrMapSearch(strmap,"LISTENER_OFFLINE")
				: t.Errors[i] == true ? StrMapSearch(strmap,"LISTENER_ERROR") : StrMapSearch(strmap, "LISTENER_ONLINE"));
			WuUniUintReplace(&tmp, L"{PORTNUM}", t.Ports[i]);
			WuEnableTag(&tmp, t.Enables[i] ? L"STOPA" : L"STARTA");
			WuUniInsertBefore(&buf, tmp, L"<!--LISTENERS-->");
			Free(tmp);
		}
		FreeRpcListenerList(&t);
		Free(tmpl);
	}
	WuUniStrReplace(&buf, L"{LINK_HUB}", WP_HUB);
	WuUniStrReplace(&buf, L"{SESSIONKEY}", sessionkey);
	WuUniStrReplace(&buf, L"{LISTENER_LINK}", WP_LISTENER);
	WuUniStrReplace(&buf, L"{LINK_NEWHUB}", WP_NEWHUB);
	WuUniStrReplace(&buf, L"{LINK_SERVER}", WP_SERVER);
	WuUniStrReplace(&buf, L"{LINK_LICENSE}", WP_LICENSE);
	WuUniStrReplace(&buf, L"{LINK_LOCALBRIDGE}", WP_LOCALBRIDGE);

	WuFreeStrStrMap(strmap);

	return buf;
}

// Listener management
static wchar_t *WpListener(WEBUI *wu, LIST *params)
{
	char *sessionkey = (char*)StrMapSearch(params, "KEY");
	WU_CONTEXT *context = WuGetContext(wu->Contexts, sessionkey);
	char *cmd = (char*)StrMapSearch(params, "CMD");
	RPC_LISTENER t;
	UINT retcode;

	if(context == NULL)
	{
		return WuRedirectPage(WP_LOGIN);
	}

	t.Port = ToInt((char*)StrMapSearch(params, "PORT"));

	if( StrCmp(cmd, "CREATE") == 0 )
	{
		//  Create a new listener
		if(t.Port == 0)
		{
			wchar_t *buf = WuUniReadFile("|"WP_LISTENER);
			WuUniStrReplace(&buf, L"ACTION", WP_LISTENER);
			WuUniStrReplace(&buf, L"SESSIONKEY", sessionkey);
			return buf;
		}
		else
		{
			t.Enable = true;
			retcode = StCreateListener(context->Admin, &t);
		}
	}
	else if( StrCmp(cmd, "DEL")==0 )
	{
		retcode = StDeleteListener(context->Admin, &t);
	}
	else if(StrCmp(cmd, "START")==0 )
	{
		t.Enable = true;
		retcode = StEnableListener(context->Admin, &t);
	}
	else if(StrCmp(cmd, "STOP")==0 )
	{
		t.Enable = false;
		retcode = StEnableListener(context->Admin, &t);
	}

	if(retcode == ERR_NO_ERROR)
	{
		char tmp[MAX_SIZE];
		Format(tmp, sizeof(tmp), "%s?KEY=%s", WP_SERVER, sessionkey);
		return WuRedirectPage(tmp);
	}
	return WuErrorPage(retcode);
}

// Virtual HUB management
static wchar_t *WpHub(WEBUI *wu, LIST *params)
{
	char *hubname = (char*)StrMapSearch(params, "HUB");
	char *sessionkey = (char*)StrMapSearch(params, "KEY");
	char *cmd  = (char*)StrMapSearch(params, "CMD");
	WU_CONTEXT *context = WuGetContext(wu->Contexts, sessionkey);

	// Confirm the session
	if(context == NULL)
	{
		return WuRedirectPage(WP_LOGIN);
	}

	if(StrCmp(cmd, "ONLINE") == 0 || StrCmp(cmd, "OFFLINE") == 0)
	{
		// Online / offline switching
		UINT retcode;
		RPC_SET_HUB_ONLINE t;
		StrCpy(t.HubName, sizeof(t.HubName), hubname);
		t.Online  = (StrCmp(cmd, "ONLINE") == 0) ? true : false;
		retcode = StSetHubOnline(context->Admin, &t);
		if(retcode == ERR_NO_ERROR){
			char tmp[MAX_SIZE];
			Format(tmp, sizeof(tmp), "%s?HUB=%s&KEY=%s", WP_HUB, hubname, sessionkey);
			return WuRedirectPage(tmp);
		}else{
			return WuErrorPage(retcode);
		}
	}
	else if(StrCmp(cmd, "DELETE") == 0)
	{
		// Delete the Virtual HUB
		RPC_DELETE_HUB t;
		UINT retcode;
		StrCpy(t.HubName, sizeof(t.HubName), hubname);
		retcode = StDeleteHub(context->Admin, &t);
		if(retcode == ERR_NO_ERROR)
		{
			char tmp[MAX_SIZE];
			Format(tmp, sizeof(tmp), "%s?KEY=%s", WP_SERVER, sessionkey);
			return WuRedirectPage(tmp);
		}
		return WuErrorPage(retcode);
	}
	else
	{
		// Show the status and commands of the virtual HUB
		RPC_HUB_STATUS t;
		UINT retcode;
		Zero(&t, sizeof(t));
		StrCpy(t.HubName, sizeof(t.HubName), hubname);
		retcode = StGetHubStatus(context->Admin, &t);
		if(retcode == ERR_NO_ERROR)
		{
			wchar_t tmp[MAX_SIZE];
			wchar_t *buf = WuUniReadFile("|"WP_HUB);
			LIST *strmap = WuUniMakeTableFromTemplate(&buf, L"<!--STRMAP:", L":STRMAP-->");

			WuUniStrReplace(&buf, L"{HUBNAME}", t.HubName);
			WuUniReplace(&buf, L"{HUBSTATE}", StrMapSearch(strmap, t.Online == false ? "HUB_OFFLINE" : "HUB_ONLINE"));
			WuUniReplace(&buf, L"{HUBTYPE}", GetHubTypeStr(t.HubType));
			WuUniReplace(&buf, L"{HUBSNAT}", StrMapSearch(strmap, t.SecureNATEnabled == false ? "SECNAT_OFF" : "SECNAT_ON"));
			WuUniUintReplace(&buf, L"{HUBSESSIONS}", t.NumSessions);
			WuUniUintReplace(&buf, L"{HUBACLS}", t.NumAccessLists);
			WuUniUintReplace(&buf, L"{HUBUSERS}", t.NumUsers);
			WuUniUintReplace(&buf, L"{HUBGROUPS}", t.NumGroups);
			WuUniUintReplace(&buf, L"{HUBMACTBLS}", t.NumMacTables);
			WuUniUintReplace(&buf, L"{HUBIPTBLS}", t.NumIpTables);
			WuUniUintReplace(&buf, L"{HUBLOGINS}", t.NumLogin);
			GetDateTimeStr64Uni(tmp, sizeof(tmp), SystemToLocal64(t.LastLoginTime));
			WuUniReplace(&buf, L"{HUBLASTLOGIN}", tmp);
			GetDateTimeStr64Uni(tmp, sizeof(tmp), SystemToLocal64(t.LastCommTime));
			WuUniReplace(&buf, L"{HUBLASTCOMM}", tmp);
			WuUniUint64Replace(&buf, L"{HUBTXUNIPKTS}", t.Traffic.Send.UnicastCount);
			WuUniUint64Replace(&buf, L"{HUBTXUNISIZE}", t.Traffic.Send.UnicastBytes);
			WuUniUint64Replace(&buf, L"{HUBTXBRPKTS}", t.Traffic.Send.BroadcastCount);
			WuUniUint64Replace(&buf, L"{HUBTXBRSIZE}", t.Traffic.Send.BroadcastBytes);
			WuUniUint64Replace(&buf, L"{HUBRXUNIPKTS}", t.Traffic.Recv.UnicastCount);
			WuUniUint64Replace(&buf, L"{HUBRXUNISIZE}", t.Traffic.Recv.UnicastBytes);
			WuUniUint64Replace(&buf, L"{HUBRXBRPKTS}", t.Traffic.Recv.BroadcastCount);
			WuUniUint64Replace(&buf, L"{HUBRXBRSIZE}", t.Traffic.Recv.BroadcastBytes);

			WuEnableTag(&buf, t.Online ? L"ENABLE_OFFLINE" : L"ENABLE_ONLINE");

			WuUniStrReplace(&buf, L"{LINK_HUB}", WP_HUB);
			WuUniStrReplace(&buf, L"{LINK_USER}", WP_USER);
			WuUniStrReplace(&buf, L"{LINK_SERVER}", WP_SERVER);
			WuUniStrReplace(&buf, L"{LINK_SESSION}", WP_SESSION);
			WuUniStrReplace(&buf, L"{HUBNAME}", hubname);
			WuUniStrReplace(&buf, L"{SESSIONKEY}", sessionkey);
			WuUniStrReplace(&buf, L"{LINK_SECURENAT}", WP_SECURENAT);

			WuFreeStrStrMap(strmap);

			return buf;
		}else{
			return WuErrorPage(retcode);
		}
	}
}

// User list
static wchar_t *WpUser(WEBUI *wu, LIST *params)
{
	char *sessionkey = (char*)StrMapSearch(params, "KEY");
	WU_CONTEXT *context = WuGetContext(wu->Contexts, sessionkey);
	char *hubname = (char*)StrMapSearch(params, "HUB");
	char *cmd = (char*)StrMapSearch(params, "CMD");
	UINT retcode;
	RPC_ENUM_USER t;

	// Check the context
	if(context == NULL)
	{
		return WuRedirectPage(WP_LOGIN);
	}

	if(cmd != NULL && StrCmp(cmd, "DEL") == 0)
	{
		char *username = (char*)StrMapSearch(params, "USER");
		RPC_DELETE_USER t;
		UINT retcode;

		StrCpy(t.HubName, sizeof(t.HubName), hubname);
		StrCpy(t.Name, sizeof(t.Name), username);
		retcode = StDeleteUser(context->Admin, &t);
		if(retcode == ERR_NO_ERROR)
		{
			char tmp[MAX_SIZE];
			Format(tmp, sizeof(tmp), "%s?HUB=%s&KEY=%s", WP_USER, hubname, sessionkey);
			return WuRedirectPage(tmp);
		}else
		{
			return WuErrorPage(retcode);
		}
	}

	Zero(&t, sizeof(t));
	StrCpy(t.HubName, sizeof(t.HubName), hubname);
	retcode = StEnumUser(context->Admin, &t);
	if(retcode == ERR_NO_ERROR)
	{
		UINT i;
		wchar_t *buf = WuUniReadFile("|"WP_USER);
		wchar_t *tmpl = WuUniGetTemplate(&buf, L"<!--USER_TMPL:", L"-->", true);
		wchar_t tmp[MAX_SIZE];
		wchar_t datestr[MAX_SIZE];

		for(i=0; i<t.NumUser; i++)
		{
			RPC_ENUM_USER_ITEM *item = &t.Users[i];
			GetDateTimeStr64Uni(datestr, sizeof(datestr), SystemToLocal64(item->LastLoginTime));
			UniFormat(tmp, sizeof(tmp), tmpl, item->Name, item->Name, item->Name, item->Realname, item->GroupName,
				item->Note, GetAuthTypeStr(item->AuthType), item->NumLogin, datestr);
			WuUniInsertBefore(&buf, tmp, L"<!--USERS-->");
		}
		WuUniStrReplace(&buf, L"{SESSIONKEY}", sessionkey);
		WuUniStrReplace(&buf, L"{HUBNAME}", hubname);
		WuUniStrReplace(&buf, L"{LINK_USER}", WP_USER);
		WuUniStrReplace(&buf, L"{LINK_EDITUSER}", WP_EDITUSER);
		WuUniStrReplace(&buf, L"{LINK_HUB}", WP_HUB);

		FreeRpcEnumUser(&t);
		Free(tmpl);
		return buf;
	}else{
		return WuErrorPage(retcode);
	}
}

// User edit page
static wchar_t *WpEditUser(WEBUI *wu, LIST *params)
{
	char *sessionkey = (char*)StrMapSearch(params, "KEY");
	WU_CONTEXT *context = WuGetContext(wu->Contexts, sessionkey);
	char *hubname = (char*)StrMapSearch(params, "HUB");
	char *username = (char*)StrMapSearch(params, "USER");
	char *cmd = (char*)StrMapSearch(params, "CMD");
	char tmp[MAX_SIZE];
	wchar_t utmp[MAX_SIZE];
	UINT retcode;
	RPC_SET_USER t;
	wchar_t *buf;

	// Check the context
	if(context == NULL)
	{
		return WuRedirectPage(WP_LOGIN);
	}

	if(cmd != NULL && (StrCmp(cmd, "SET") == 0 || StrCmp(cmd, "CREATE") == 0))
	{
		char *authtype = (char*)StrMapSearch(params, "AUTHTYPE");
		char *password = (char*)StrMapSearch(params, "PASSWORD");
		char *password2 = (char*)StrMapSearch(params, "PASSWORD2");
		bool create = (StrCmp(cmd, "CREATE") == 0);

		Zero(&t, sizeof(t));
		StrCpy(t.Name, sizeof(t.Name), username);
		StrCpy(t.HubName, sizeof(t.HubName), hubname);

		// Read the original user data in the case of edit mode.
		if(!create)
		{
			retcode = StGetUser(context->Admin, &t);
			if(retcode != ERR_NO_ERROR)
			{
				return WuErrorPage(retcode);
			}
		}

		// Set the authentication method
		if(StrCmp(authtype, "ANONYMOUS") == 0)
		{
			FreeAuthData(t.AuthType, t.AuthData);
			t.AuthType = AUTHTYPE_ANONYMOUS;
			t.AuthData = NULL;
		}
		else if(StrCmp(authtype, "PASSWORD") == 0)
		{
			if(StrCmp(password,password2) != 0)
			{
				// Password for confirmation is mismatched
				return WuErrorPage(ERR_INVALID_PARAMETER);
			}

			// If the password field has not changed, leave as is
			if(t.AuthType != AUTHTYPE_PASSWORD || StrCmp(password, WU_PASSWORD_NOCHANGE) != 0)
			{
				FreeAuthData(t.AuthType, t.AuthData);
				t.AuthType = AUTHTYPE_PASSWORD;
				t.AuthData = NewPasswordAuthData(username, password);
			}
		}
		else
		{
			// Parameters of the authentication method is invalid
			return WuErrorPage(ERR_INVALID_PARAMETER);
		}

		// Set the user information
		if(create)
		{
			retcode = StCreateUser(context->Admin, &t);
		}
		else
		{
			retcode = StSetUser(context->Admin, &t);
		}
		if(retcode != ERR_NO_ERROR)
		{
			return WuErrorPage(retcode);
		}

		Format(tmp, sizeof(tmp), "%s?KEY=%s&HUB=%s", WP_USER, sessionkey, hubname);
		return WuRedirectPage(tmp);
	}

	// Generate the user edit page
	Zero(&t, sizeof(t));
	StrCpy(t.HubName, sizeof(t.HubName), hubname);

	if( username == NULL )
	{
		t.AuthType = AUTHTYPE_PASSWORD;
	}else{
		UINT retcode;
		StrCpy(t.Name, sizeof(t.Name), username);
		retcode = StGetUser(context->Admin, &t);
		if(retcode != ERR_NO_ERROR)
		{
			return WuErrorPage(retcode);
		}
	}

	buf = WuUniReadFile("|"WP_EDITUSER);

	if( username == NULL)
	{
		WuEnableTag(&buf, L"{USERNAMEINPUT}");
		WuUniReplace(&buf, L"{USERNAME}", L"");
		WuUniReplace(&buf, L"{CMDNAME}", L"CREATE");
		WuUniReplace(&buf, L"{PASSWORD}", L"");
	}
	else
	{
		WuEnableTag(&buf, L"{USERNAMEHIDDEN}");
		WuUniStrReplace(&buf, L"{USERNAME}", username);
		WuUniReplace(&buf, L"{CMDNAME}", L"SET");
		WuUniStrReplace(&buf, L"{PASSWORD}", WU_PASSWORD_NOCHANGE);
	}

	// Select the authentication method
	if(t.AuthType == AUTHTYPE_ANONYMOUS)
	{
		WuUniReplace(&buf, L"{SELANONYM}", L"checked");
	}
	else
	{
		WuUniReplace(&buf, L"{SELANONYM}", L"");
		if(t.AuthType == AUTHTYPE_PASSWORD)
		{
			WuUniReplace(&buf, L"{SELPASSWD}", L"checked");
		}
		else
		{
			WuUniReplace(&buf, L"{SELPASSWD}", L"");
		}
	}

	WuUniReplace(&buf, L"{REALNAME}", t.Realname);
	WuUniReplace(&buf, L"{NOTETEXT}", t.Note);
	WuUniStrReplace(&buf, L"{GROUPNAME}", t.GroupName);
	GetDateTimeStr64Uni(utmp, sizeof(utmp), SystemToLocal64(t.ExpireTime));
	WuUniReplace(&buf, L"{EXPIREDATE}", utmp);

	WuUniStrReplace(&buf, L"{SESSIONKEY}", sessionkey);
	WuUniStrReplace(&buf, L"{HUBNAME}", hubname);
	WuUniStrReplace(&buf, L"{LINK_EDITUSER}", WP_EDITUSER);
	WuUniStrReplace(&buf, L"{LINK_USER}", WP_USER);

	return buf;
}

// Create a new Virtual HUB
static wchar_t *WpNewHub(WEBUI *wu, LIST *params)
{
	char *sessionkey = (char*)StrMapSearch(params, "KEY");
	WU_CONTEXT *context = WuGetContext(wu->Contexts, sessionkey);
	char *cmd = (char*)StrMapSearch(params, "CMD");
	wchar_t *buf;

	if(context == NULL)
	{
		return WuRedirectPage(WP_LOGIN);
	}

	if(StrCmp(cmd, "CREATE") == 0)
	{
		UINT retcode;
		char tmp[MAX_SIZE];
		RPC_CREATE_HUB t;
		RPC_SERVER_INFO t2;
		char *hubname = (char*)StrMapSearch(params, "NAME");
		char *passwd = (char*)StrMapSearch(params, "PASSWD");
		char *passwd2 = (char*)StrMapSearch(params, "PASSWD2");

		if(strcmp(passwd,passwd2) != 0)
		{
			return WuErrorPage(ERR_INVALID_PARAMETER);
		}

		Zero(&t, sizeof(t));
		StrCpy(t.HubName, sizeof(t.HubName), hubname);
		Hash(t.HashedPassword, passwd, StrLen(passwd), true);
		HashPassword(t.SecurePassword, ADMINISTRATOR_USERNAME, passwd);
		t.Online = true;
		t.HubType = HUB_TYPE_STANDALONE;

		// Set to dynamic HUB in the case of cluster controller
		Zero(&t2, sizeof(t2));
		if (StGetServerInfo(context->Admin, &t2) == ERR_NO_ERROR)
		{
			if (t2.ServerType == SERVER_TYPE_FARM_CONTROLLER)
			{
				t.HubType = HUB_TYPE_FARM_DYNAMIC;
			}
			FreeRpcServerInfo(&t2);
		}
		
		retcode = StCreateHub(context->Admin, &t);
		if(retcode != ERR_NO_ERROR)
		{
			return WuErrorPage(retcode);
		}

		Format(tmp, sizeof(tmp), "%s?KEY=%s", WP_SERVER, sessionkey);
		return WuRedirectPage(tmp);
	}

	buf = WuUniReadFile("|"WP_NEWHUB);
	WuUniStrReplace(&buf, L"{SESSIONKEY}", sessionkey);
	WuUniStrReplace(&buf, L"{LINK_NEWHUB}", WP_NEWHUB);
	WuUniStrReplace(&buf, L"{LINK_SERVER}", WP_SERVER);
	return buf;
}

// License management page
static wchar_t *WpLicense(WEBUI *wu, LIST *params)
{
	char *sessionkey = (char*)StrMapSearch(params, "KEY");
	WU_CONTEXT *context = WuGetContext(wu->Contexts, sessionkey);
	char *cmd = (char*)StrMapSearch(params, "CMD");
	UINT retcode;
	wchar_t *buf;
	LIST *strmap;

	if(context == NULL)
	{
		return WuRedirectPage(WP_LOGIN);
	}

	// Add a license
	if(StrCmp(cmd, "ADD") == 0)
	{
		RPC_TEST t;
		char tmp[MAX_SIZE];
		char *licensekey = (char*)StrMapSearch(params, "KEYSTRINGS");


		Zero(&t, sizeof(t));
		StrCpy(t.StrValue, sizeof(t.StrValue), licensekey); 
		
		retcode = StAddLicenseKey(context->Admin, &t);
		if(retcode != ERR_NO_ERROR)
		{
			return WuErrorPage(retcode);
		}

		Format(tmp, sizeof(tmp), "%s?KEY=%s", WP_LICENSE, sessionkey);
		return WuRedirectPage(tmp);
		
	}
	// Remove the license
	else if(StrCmp(cmd, "DEL") == 0)
	{
		RPC_TEST t;
		char tmp[MAX_SIZE];
		char *id = (char*)StrMapSearch(params, "ID");

		Zero(&t, sizeof(t));
		t.IntValue = ToInt(id);
		
		retcode = StDelLicenseKey(context->Admin, &t);
		if(retcode != ERR_NO_ERROR)
		{
			return WuErrorPage(retcode);
		}

		Format(tmp, sizeof(tmp), "%s?KEY=%s", WP_LICENSE, sessionkey);
		return WuRedirectPage(tmp);
	}

	buf = WuUniReadFile("|"WP_LICENSE);
	strmap = WuUniMakeTableFromTemplate(&buf, L"<!--STRMAP:", L"-->");

	// Enumerate the license keys
	{
		UINT i;
		RPC_ENUM_LICENSE_KEY t;
		wchar_t *tmpl;
		Zero(&t, sizeof(t));

		retcode = StEnumLicenseKey(context->Admin, &t);

		if(retcode != ERR_NO_ERROR)
		{
			WuFreeStrStrMap(strmap);
			Free(buf);
			return WuErrorPage(retcode);
		}

		tmpl = WuUniGetTemplate(&buf, L"<!--LICENSES_TMPL:", L":LICENSES_TMPL-->", true);

		for(i = 0; i < t.NumItem; i++)
		{
			wchar_t *status, expires[128];
			wchar_t *tmp = UniCopyStr(tmpl);
			RPC_ENUM_LICENSE_KEY_ITEM *item = &t.Items[i];

			status = LiGetLicenseStatusStr(item->Status);

			if(item->Expires == 0)
			{
				UniStrCpy(expires, sizeof(expires), StrMapSearch(strmap, "EXPIRE_INFINITE"));
			}
			else
			{
				GetDateStrEx64(expires, sizeof(expires), item->Expires, NULL);
			}

			WuUniUintReplace(&tmp, L"{ID}", i);
			WuUniStrReplace(&tmp, L"{LICENSEKEY}", item->LicenseKey);
			WuUniStrReplace(&tmp, L"{LICENSENAME}", item->LicenseName);
			WuUniReplace(&tmp, L"{STATUS}", status);
			WuUniReplace(&tmp, L"{EXPIRES}", expires);
			WuUniStrReplace(&tmp, L"{LICENSEID}", item->LicenseId);
			WuUniUintReplace(&tmp, L"{PRODUCTID}", item->ProductId);
			WuUniUint64Replace(&tmp, L"{SYSTEMID}", item->SystemId);
			WuUniUintReplace(&tmp, L"{SERIALID}", item->SerialId);

			WuUniInsertBefore(&buf, tmp, L"<!--LICENSES-->");
			Free(tmp);
		}
		FreeRpcEnumLicenseKey(&t);
		Free(tmpl);
	}

	// Show the license status
	{	
		RPC_LICENSE_STATUS t;

		Zero(&t, sizeof(t));

		retcode = StGetLicenseStatus(context->Admin, &t);

		if(retcode != ERR_NO_ERROR)
		{
			WuFreeStrStrMap(strmap);
			Free(buf);
			return WuErrorPage(retcode);
		}

		WuUniStrReplace(&buf, L"{LSEDITIONNAME}", t.EditionStr);
		WuUniUint64Replace(&buf, L"{LSSERVERID}", t.SystemId);
		if(t.SystemExpires == 0)
		{
			WuUniReplace(&buf, L"{LSEXPIRES}", StrMapSearch(strmap, "NOEXPIRE"));
		}
		else
		{	
			wchar_t expires[128];
			GetDateStrEx64(expires, sizeof(expires), t.SystemExpires, NULL);
			WuUniReplace(&buf, L"{LSEXPIRES}", expires);
		}
		
		if(t.NumBridgeConnectLicense == INFINITE)
		{
			WuUniReplace(&buf, L"{LSNUMBRIDGES}", StrMapSearch(strmap, "LICENSE_INFINITE"));
		}
		else
		{
			WuUniUintReplace(&buf, L"{LSNUMBRIDGES}", t.NumBridgeConnectLicense);
		}

		if(t.NumClientConnectLicense == INFINITE)
		{
			WuUniReplace(&buf, L"{LSNUMCLIENTS}", StrMapSearch(strmap, "LICENSE_INFINITE"));
		}
		else
		{
			WuUniUintReplace(&buf, L"{LSNUMCLIENTS}", t.NumClientConnectLicense);
		}

	}

	WuUniStrReplace(&buf, L"{LINK_LICENSE}", WP_LICENSE);
	WuUniStrReplace(&buf, L"{LINK_SERVER}", WP_SERVER);
	WuUniStrReplace(&buf, L"{SESSIONKEY}", sessionkey);
	WuFreeStrStrMap(strmap);

	return buf;

}

// Local bridge setup page
static wchar_t *WpLocalBridge(WEBUI *wu, LIST *params)
{
	char *sessionkey = (char*)StrMapSearch(params, "KEY");
	WU_CONTEXT *context = WuGetContext(wu->Contexts, sessionkey);
	char *cmd = (char*)StrMapSearch(params, "CMD");
	UINT retcode;
	wchar_t *buf;
	LIST *strmap;

	if(context == NULL)
	{
		return WuRedirectPage(WP_LOGIN);
	}

	// Create a local bridge
	if(StrCmp(cmd, "CREATE") == 0)
	{
		RPC_LOCALBRIDGE t;
		RPC_ENUM_ETH eth;
		char tmp[MAX_SIZE];
		char *tapmode = (char*)StrMapSearch(params, "TAPMODE");
		char *tapname = (char*)StrMapSearch(params, "TAPNAME");
		char *devid = (char*)StrMapSearch(params, "DEVID");
		char *hubname = (char*)StrMapSearch(params, "LBHUBNAME");
		UINT id = ToInt(devid);

		Zero(&eth, sizeof(eth));
		retcode = StEnumEthernet(context->Admin, &eth);

		if(retcode != ERR_NO_ERROR)
		{
			FreeRpcEnumEth(&eth);
			return WuErrorPage(retcode);
		}

		Zero(&t, sizeof(t));
		t.Active = true;
		if(StrCmp(tapmode, "YES") == 0)
		{	
			t.TapMode = true;
			StrCpy(t.DeviceName, sizeof(t.DeviceName), tapname);
		}
		else
		{
			t.TapMode = false;
			StrCpy(t.DeviceName, sizeof(t.DeviceName), eth.Items[id].DeviceName);
		}
		StrCpy(t.HubName, sizeof(t.HubName), hubname);
		t.Online = true;
		FreeRpcEnumEth(&eth);

		retcode = StAddLocalBridge(context->Admin, &t);
		
		if(retcode != ERR_NO_ERROR)
		{
			return WuErrorPage(retcode);
		}

		Format(tmp, sizeof(tmp), "%s?KEY=%s", WP_LOCALBRIDGE, sessionkey);
		return WuRedirectPage(tmp);
	}

	// Delete the local bridge
	if(StrCmp(cmd, "DEL") == 0)
	{
		RPC_LOCALBRIDGE t;
		RPC_ENUM_LOCALBRIDGE et;
		char tmp[MAX_SIZE];
		char *listid = (char*)StrMapSearch(params, "LISTID");
		UINT id = ToInt(listid);

		Zero(&et, sizeof(et));
		retcode = StEnumLocalBridge(context->Admin, &et);
		if(retcode != ERR_NO_ERROR)
		{
			return WuErrorPage(retcode);
		}

		Zero(&t, sizeof(t));
		StrCpy(t.DeviceName, sizeof(t.DeviceName), et.Items[id].DeviceName);
		StrCpy(t.HubName, sizeof(t.HubName), et.Items[id].HubName);
		FreeRpcEnumLocalBridge(&et);
		
		retcode = StDeleteLocalBridge(context->Admin, &t);

		if(retcode != ERR_NO_ERROR)
		{
			return WuErrorPage(retcode);
		}

		Format(tmp, sizeof(tmp), "%s?KEY=%s", WP_LOCALBRIDGE, sessionkey);
		return WuRedirectPage(tmp);
	}

	// Show the current local bridge list and the Virtual HUB list and the device list to be bridged
	buf = WuUniReadFile("|"WP_LOCALBRIDGE);
	strmap = WuUniMakeTableFromTemplate(&buf, L"<!--STRMAP:", L"-->");

	// Show the current local bridge list
	{
		UINT i;
		RPC_ENUM_LOCALBRIDGE t;
		wchar_t *tmpl;

		Zero(&t, sizeof(t));
		retcode = StEnumLocalBridge(context->Admin, &t);

		if(retcode != ERR_NO_ERROR)
		{
			FreeRpcEnumLocalBridge(&t);
			Free(buf);
			WuFreeStrStrMap(strmap);
			return WuErrorPage(retcode);
		}

		tmpl = WuUniGetTemplate(&buf, L"<!--LBLIST_TMPL", L"LBLIST_TMPL-->", true);

		for(i = 0; i < t.NumItem; i++)
		{
			RPC_LOCALBRIDGE *item = &t.Items[i];
			wchar_t *tmp = UniCopyStr(tmpl);
			WuUniUintReplace(&tmp, L"{LISTID}", i);
			WuUniStrReplace(&tmp, L"{HUBNAME}", item->HubName);
			WuUniStrReplace(&tmp, L"{DEVICENAME}", item->DeviceName);
			WuUniReplace(&tmp, L"{STATUS}", item->Online ? item->Active ? StrMapSearch(strmap, "BRIDGE_ONLINE") 
				: StrMapSearch(strmap, "BRIDGE_ERROR") : StrMapSearch(strmap, "BRIDGE_OFFLINE"));
			WuUniInsertBefore(&buf, tmp, L"<!--LBLIST-->");
			Free(tmp);
		}
		Free(tmpl);
		FreeRpcEnumLocalBridge(&t);
	}

	// Show the Virtual HUB list
	{
		wchar_t *tmpl;
		RPC_ENUM_HUB t;
		UINT i;

		Zero(&t, sizeof(t));
		retcode = StEnumHub(context->Admin, &t);

		if(retcode != ERR_NO_ERROR)
		{
			FreeRpcEnumHub(&t);
			Free(buf);
			WuFreeStrStrMap(strmap);
			return WuErrorPage(retcode);
		}

		tmpl = WuUniGetTemplate(&buf, L"<!--HUBS_TMPL", L"HUBS_TMPL-->", true);
		for(i=0; i<t.NumHub; i++)
		{
			wchar_t *tmp = UniCopyStr(tmpl);
			WuUniStrReplace(&tmp, L"{LBHUBNAME}", t.Hubs[i].HubName);
			WuUniInsertBefore(&buf, tmp, L"<!--HUBS-->");
			Free(tmp);
		}
		FreeRpcEnumHub(&t);
		Free(tmpl);
	}

	// Get the device list to be bridge
	{
		UINT i;
		RPC_ENUM_ETH t;
		wchar_t *tmpl;

		Zero(&t, sizeof(t));
		retcode = StEnumEthernet(context->Admin, &t);

		if(retcode != ERR_NO_ERROR)
		{
			FreeRpcEnumEth(&t);
			Free(buf);
			WuFreeStrStrMap(strmap);
			return WuErrorPage(retcode);
		}

		tmpl = WuUniGetTemplate(&buf, L"<!--LBDEVLIST_TMPL", L"LBDEVLIST_TMPL-->", true);

		for(i = 0; i < t.NumItem; i++)
		{
			wchar_t *tmp = UniCopyStr(tmpl);
			WuUniUintReplace(&tmp, L"{DEVID}", i);
			WuUniStrReplace(&tmp, L"{ABLEDEVICE}", t.Items[i].DeviceName);
			WuUniInsertBefore(&buf, tmp, L"<!--LBDEVLIST-->");
			Free(tmp);
		}
		FreeRpcEnumEth(&t);
		Free(tmpl);
	}

	WuUniStrReplace(&buf, L"{LINK_LOCALBRIDGE}", WP_LOCALBRIDGE);
	WuUniStrReplace(&buf, L"{LINK_SERVER}", WP_SERVER);
	WuUniStrReplace(&buf, L"{SESSIONKEY}", sessionkey);
	WuFreeStrStrMap(strmap);

	return buf;

}

// Configure the virtual DHCP function and virtual NAT (SecureNAT)
static wchar_t *WpSecureNAT(WEBUI *wu, LIST *params)
{
	char *sessionkey = (char*)StrMapSearch(params, "KEY");

	WU_CONTEXT *context = WuGetContext(wu->Contexts, sessionkey);
	char *cmd = (char*)StrMapSearch(params, "CMD");
	char *hubname = (char*)StrMapSearch(params, "HUB");

	UINT retcode;
	wchar_t *buf;

	if(context == NULL)
	{
		return WuRedirectPage(WP_LOGIN);
	}

	// Enable / disable the SecureNAT function
	if(StrCmp(cmd, "ENABLE") == 0 || StrCmp(cmd, "DISABLE") == 0)
	{
		RPC_HUB t;
		char tmp[MAX_SIZE];

		Zero(&t, sizeof(t));
		StrCpy(t.HubName, sizeof(t.HubName), hubname);

		if(StrCmp(cmd, "ENABLE") == 0)
		{
			retcode = StEnableSecureNAT(context->Admin, &t);
		}
		else
		{
			retcode = StDisableSecureNAT(context->Admin, &t);
		}

		if(retcode != ERR_NO_ERROR)
		{
			return WuErrorPage(retcode);
		}

		Format(tmp, sizeof(tmp), "%s?HUB=%s&KEY=%s", WP_SECURENAT, hubname, sessionkey);
		return WuRedirectPage(tmp);
	}
	// Set the SecureNAT options
	else if(StrCmp(cmd, "SAVE") == 0)
	{
		char tmp[MAX_SIZE];
		VH_OPTION t;

		Zero(&t, sizeof(t));
		StrCpy(t.HubName, sizeof(t.HubName), hubname);

		// Configure the a virtual host
		StrToMac(t.MacAddress, (char*)StrMapSearch(params, "HOSTMAC"));
		StrToIP(&t.Ip, (char*)StrMapSearch(params, "HOSTIP"));
		StrToIP(&t.Mask, (char*)StrMapSearch(params, "HOSTMASK"));

		// Configure the virtual NAT function
		t.UseNat = (StrCmp((char*)StrMapSearch(params, "NATCHECK"), "on") == 0);
		t.Mtu = ToInt((char*)StrMapSearch(params, "NATMTU"));
		t.NatTcpTimeout = ToInt((char*)StrMapSearch(params, "NATTCPTIMEOUT"));
		t.NatUdpTimeout = ToInt((char*)StrMapSearch(params, "NATUDPTIMEOUT"));
		t.SaveLog = (StrCmp((char*)StrMapSearch(params, "NATSAVELOG"), "on") == 0);

		// Configure the virtual DHCP server function
		t.UseDhcp = (StrCmp((char*)StrMapSearch(params, "DHCPCHECK"), "on") == 0);
		StrToIP(&t.DhcpLeaseIPStart, (char*)StrMapSearch(params, "DHCPIPS"));
		StrToIP(&t.DhcpLeaseIPEnd, (char*)StrMapSearch(params, "DHCPIPE"));
		StrToIP(&t.DhcpSubnetMask, (char*)StrMapSearch(params, "DHCPMASK"));
		t.DhcpExpireTimeSpan =  ToInt((char*)StrMapSearch(params, "DHCPEXPIRE"));
		StrToIP(&t.DhcpGatewayAddress, (char*)StrMapSearch(params, "DHCPGW"));
		StrToIP(&t.DhcpDnsServerAddress, (char*)StrMapSearch(params, "DHCPDNS"));
		StrCpy(t.DhcpDomainName, sizeof(t.DhcpDomainName), (char*)StrMapSearch(params, "DHCPDOMAIN"));

		retcode = StSetSecureNATOption(context->Admin, &t);
		if(retcode == ERR_NO_ERROR)
		{
			Format(tmp, sizeof(tmp), "%s?HUB=%s&KEY=%s", WP_SECURENAT, hubname, sessionkey);
			return WuRedirectPage(tmp);
		}
		return WuErrorPage(retcode);
	}

	buf = WuUniReadFile("|"WP_SECURENAT);

	// Get the enable / disable state of the current SecureNAT
	{
		RPC_HUB_STATUS t;
		Zero(&t, sizeof(&t));
		StrCpy(t.HubName, sizeof(t.HubName), hubname);

		retcode = StGetHubStatus(context->Admin, &t);

		if(retcode != ERR_NO_ERROR)
		{
			Free(buf);
			return WuErrorPage(retcode);
		}

		WuEnableTag(&buf, t.SecureNATEnabled ? L"DISABLESNAT" : L"ENABLESNAT");
	}

	// Show the advanced settings of the current SecureNAT
	{
		char mac[MAX_SIZE], ip[MAX_SIZE], mask[MAX_SIZE];
		char dhcpips[MAX_SIZE], dhcpipe[MAX_SIZE], dhcpmask[MAX_SIZE];
		char optgw[MAX_SIZE], optdns[MAX_SIZE];
		VH_OPTION t;

		Zero(&t, sizeof(t));
		StrCpy(t.HubName, sizeof(t.HubName), hubname);

		retcode = StGetSecureNATOption(context->Admin, &t);


		// Configure the virtual interfaces
		MacToStr(mac, sizeof(mac), t.MacAddress);
		IPToStr(ip, sizeof(ip), &t.Ip);
		IPToStr(mask, sizeof(mask), &t.Mask);
		WuUniStrReplace(&buf, L"{HOSTMAC}", mac);
		WuUniStrReplace(&buf, L"{HOSTIP}", ip);
		WuUniStrReplace(&buf, L"{HOSTMASK}", mask);

		// Configure the Virtual NAT
		WuUniStrReplace(&buf, L"{NATCHECK}", t.UseNat ? "CHECKED" : "");
		WuUniUintReplace(&buf, L"{NATMTU}", t.Mtu);
		WuUniUintReplace(&buf, L"{NATTCPTIMEOUT}", t.NatTcpTimeout);
		WuUniUintReplace(&buf, L"{NATUDPTIMEOUT}", t.NatUdpTimeout);

		WuUniStrReplace(&buf, L"{NATSAVELOG}", t.SaveLog ? "CHECKED" : "");

		// Configure the Virtual DHCP server
		WuUniStrReplace(&buf, L"{DHCPCHECK}", t.UseDhcp ? "CHECKED" : "");

		IPToStr(dhcpips, sizeof(dhcpips), &t.DhcpLeaseIPStart);
		IPToStr(dhcpipe, sizeof(dhcpipe), &t.DhcpLeaseIPEnd);
		IPToStr(dhcpmask, sizeof(dhcpmask), &t.DhcpSubnetMask);
		WuUniStrReplace(&buf, L"{DHCPIPS}", dhcpips);
		WuUniStrReplace(&buf, L"{DHCPIPE}", dhcpipe);
		WuUniStrReplace(&buf, L"{DHCPMASK}", dhcpmask);
		WuUniUintReplace(&buf, L"{DHCPEXPIRE}", t.DhcpExpireTimeSpan);

		IPToStr(optgw, sizeof(optgw), &t.DhcpGatewayAddress);
		IPToStr(optdns, sizeof(optdns), &t.DhcpDnsServerAddress);
		WuUniStrReplace(&buf, L"{DHCPGW}", optgw);
		WuUniStrReplace(&buf, L"{DHCPDNS}", optdns);
		WuUniStrReplace(&buf, L"{DHCPDOMAIN}", t.DhcpDomainName);
	}

	WuUniStrReplace(&buf, L"{LINK_HUB}", WP_HUB);
	WuUniStrReplace(&buf, L"{LINK_SECURENAT}", WP_SECURENAT);
	WuUniStrReplace(&buf, L"{HUBNAME}", hubname);
	WuUniStrReplace(&buf, L"{SESSIONKEY}", sessionkey);

	return buf;
}

static wchar_t *WpSession(WEBUI *wu, LIST *params)
{
	char *sessionkey = (char*)StrMapSearch(params, "KEY");
	WU_CONTEXT *context = WuGetContext(wu->Contexts, sessionkey);
	char *hub = (char*)StrMapSearch(params, "HUB");
	char *cmd = (char*)StrMapSearch(params, "CMD");

	if(context == NULL)
	{
		return WuRedirectPage(WP_LOGIN);
	}

	if(StrCmp(cmd, "DEL") == 0)
	{
		char *session = (char*)StrMapSearch(params, "SESSION");
		RPC_DELETE_SESSION t;
		UINT retcode;
		char tmp[MAX_SIZE];

		Zero(&t, sizeof(t));
		StrCpy(t.HubName, sizeof(t.HubName), hub);
		StrCpy(t.Name, sizeof(t.Name), session);

		retcode = StDeleteSession(context->Admin, &t);
		if(retcode != ERR_NO_ERROR)
		{
			return WuErrorPage(retcode);
		}

		Format(tmp, sizeof(tmp), "%s?HUB=%s&KEY=%s", WP_SESSION, hub, session);
		return WuRedirectPage(tmp);
	}

	// Show the session list
	{
		RPC_ENUM_SESSION t;
		UINT retcode;

		Zero(&t, sizeof(t));
		StrCpy(t.HubName, sizeof(t.HubName), hub);

		retcode = StEnumSession(context->Admin, &t);
		if(retcode != ERR_NO_ERROR)
		{
			return WuErrorPage(retcode);
		}else{
			wchar_t *buf = WuUniReadFile("|"WP_SESSION);
			wchar_t *tmpl = WuUniGetTemplate(&buf, L"<!--SESSION_TMPL:", L":SESSION_TMPL-->", true);
			UINT i;
			for(i=0; i<t.NumSession; i++){
				RPC_ENUM_SESSION_ITEM *item = &(t.Sessions[i]);
				wchar_t *tmp = CopyUniStr(tmpl);

				WuUniStrReplace(&tmp, L"{SESSION_NAME}", item->Name);
				WuUniStrReplace(&tmp, L"{SESSION_SERVER}", item->RemoteHostname);
				WuUniStrReplace(&tmp, L"{SESSION_USER}", item->Username);
				WuUniStrReplace(&tmp, L"{SESSION_HOST}", item->Hostname);
				WuUniUintReplace(&tmp, L"{SESSION_TCP}", item->CurrentNumTcp);
				WuUniUint64Replace(&tmp, L"{SESSION_BYTES}", item->PacketSize);
				WuUniUint64Replace(&tmp, L"{SESSION_PKTS}", item->PacketNum);
				WuUniStrReplace(&tmp, L"{SESSION}", item->Name);

				WuUniInsertBefore(&buf, tmp, L"<!--SESSIONS-->");
				Free(tmp);
			}

			WuUniStrReplace(&buf, L"{HUBNAME}", hub);
			WuUniStrReplace(&buf, L"{SESSIONKEY}", sessionkey);
			WuUniStrReplace(&buf, L"{LINK_HUB}", WP_HUB);
			WuUniStrReplace(&buf, L"{LINK_SESSION}", WP_SESSION);

			FreeRpcEnumSession(&t);
			Free(tmpl);
			return buf;
		}
	}
}


// **** Public interface of the WebUI module

// Get the page
WU_WEBPAGE *WuGetPage(char *target, WEBUI *wu)
{
	char filename[MAX_SIZE];
	LIST *params;
	wchar_t *(*handler)(WEBUI *wu, LIST *params);

	// Delete the expired session keys
	WuExpireSessionKey(wu);

	params = WuAnalyzeTarget(target, filename, sizeof(filename));

	// Search for the handler corresponding to the URL
	handler = StrMapSearch(wu->PageList, filename);

	// Call the handler
	if(handler != NULL)
	{
		wchar_t *unitmp;
		WU_WEBPAGE *page;
		unitmp = handler(wu, params);
		WuFreeStrStrMap(params);
		page = WuNewUniWebPage(unitmp);
		Free(unitmp);
		return page;
	}

	WuFreeStrStrMap(params);

	// If it missed, try to read the file directly
	if(StartWith(filename, WP_DEFAULT))
	{
		char tmp[MAX_SIZE] = "|";
		BUF *buf;
		WU_WEBPAGE *page;

		StrCat(tmp, sizeof(tmp), filename);
		buf = ReadDump(tmp);
		if(buf == NULL)
		{
			return NULL;
		}

		page = WuNewWebPage(buf->Buf, buf->Size, filename);
		FreeBuf(buf);
		return page;
	}

	return NULL;
}

// Start the WebUI
WEBUI *WuNewWebUI(CEDAR *cedar)
{
	WEBUI *wu = (WEBUI*)Malloc(sizeof(WEBUI));
	int i;

	wu->Cedar = cedar;

	wu->PageList = NewStrMap();
	for(i=0;i<sizeof(wu_pages)/sizeof(STRMAP_ENTRY);i++)
	{
		Add(wu->PageList, &wu_pages[i]);
	}

	wu->Contexts = NewStrMap();

	return wu;
}

// Release the WebUI
bool WuFreeWebUI(WEBUI *wu)
{
	UINT i;

	if(wu == NULL)
	{
		return false;
	}

	for(i=0; i<LIST_NUM(wu->Contexts); i++)
	{
		STRMAP_ENTRY *se = LIST_DATA(wu->Contexts, i);
		Free(se->Name);
		WuFreeContext((WU_CONTEXT*)se->Value);
		Free(se);
	}
	ReleaseList(wu->Contexts);

	ReleaseList(wu->PageList);
	Free(wu);
	return true;
}

void WuFreeWebPage(WU_WEBPAGE *page)
{
	if(page == NULL)
	{
		return;
	}

	FreeHttpHeader(page->header);
	Free(page->data);
	Free(page);
}

//  **** Module local utility functions

// Initialize the context
static WU_CONTEXT *WuNewContext(WEBUI *wu, char *hubname)
{
	WU_CONTEXT *context = (WU_CONTEXT*)Malloc(sizeof(WU_CONTEXT));

	if(StrLen(hubname) == 0)
	{
		hubname=NULL;
	}
	context->Admin = (ADMIN*)ZeroMalloc(sizeof(ADMIN));
	context->Admin->HubName = hubname != NULL ? CopyStr(hubname) : NULL;
	context->Admin->Server = wu->Cedar->Server;
	context->Admin->ServerAdmin = hubname == NULL ? true: false;
	context->Admin->Rpc = (RPC*)ZeroMalloc(sizeof(RPC));
	StrCpy(context->Admin->Rpc->Name, sizeof(context->Admin->Rpc->Name), "WEBUI");
	context->Admin->Rpc->Param = context->Admin;
	context->Admin->Rpc->ServerAdminMode = context->Admin->ServerAdmin;
	context->Admin->Rpc->ServerMode = true;
	context->Admin->Rpc->IsVpnServer = true;
	context->Admin->Rpc->Lock = NewLock();
	context->Admin->LogFileList = NULL;

	context->ExpireDate = Tick64() + WU_CONTEXT_EXPIRE;

	return context;
}

// Release the context
static void WuFreeContext(WU_CONTEXT *context)
{
	DeleteLock(context->Admin->Rpc->Lock);
	Free(context->Admin->Rpc);
	Free(context->Admin->HubName);
	Free(context->Admin);
	Free(context);
}

// Create a WebPage structure from the Unicode string
static WU_WEBPAGE *WuNewUniWebPage(wchar_t *content)
{
	WU_WEBPAGE *ret;

	if(content == NULL)
	{
		return NULL;
	}

	ret = (WU_WEBPAGE*)Malloc(sizeof(WU_WEBPAGE));
	ret->size = CalcUniToUtf8(content);
	ret->data = (char*)Malloc(ret->size);
	UniToUtf8(ret->data, ret->size, content);

	ret->header = NewHttpHeader("HTTP/1.1", "202", "OK");
	AddHttpValue(ret->header, NewHttpValue("Content-Type", HTTP_CONTENT_TYPE4));
	AddHttpValue(ret->header, NewHttpValue("Connection", "Keep-Alive"));
	AddHttpValue(ret->header, NewHttpValue("Keep-Alive", HTTP_KEEP_ALIVE));

	return ret;
}

// Generate the WebPage structure from the raw data
static WU_WEBPAGE *WuNewWebPage(char *content, UINT size, char *filename)
{
	WU_WEBPAGE *ret;

	if(content == NULL)
	{
		return NULL;
	}

	ret = (WU_WEBPAGE*)Malloc(sizeof(WU_WEBPAGE));
	ret->size = size;
	ret->data = (char*)Malloc(size);
	Copy(ret->data, content, size);

	ret->header = NewHttpHeader("HTTP/1.1", "202", "OK");
	AddHttpValue(ret->header, NewHttpValue("Connection", "Keep-Alive"));
	AddHttpValue(ret->header, NewHttpValue("Keep-Alive", HTTP_KEEP_ALIVE));

	if(EndWith(filename, "jpg"))
	{
		AddHttpValue(ret->header, NewHttpValue("Content-Type", HTTP_CONTENT_TYPE3));
	}
	AddHttpValue(ret->header, NewHttpValue("Content-Type", HTTP_CONTENT_TYPE4));

	return ret;
}

// Return an error page
static wchar_t *WuErrorPage(UINT errorcode)
{
	wchar_t *buf = WuUniReadFile("|"WP_ERROR);
	wchar_t tmp[MAX_SIZE];
	UniFormat(tmp, sizeof(tmp), L"%d\n<H2>%s</H2>", errorcode, GetUniErrorStr(errorcode));
	WuUniReplace(&buf, L"ERRMSG", tmp);
	return buf;
}

// Redirect
static wchar_t *WuRedirectPage(char *url)
{
	wchar_t *buf = WuUniReadFile("|"WP_REDIRECT);
	wchar_t tmp[MAX_SIZE];
	StrToUni(tmp, sizeof(tmp), url);
	WuUniReplace(&buf, L"REDIRECT_TO", tmp);
	return buf;
}

// Analyse the URL
static LIST *WuAnalyzeTarget(char *target,char *filename, UINT size)
{
	char *start, tmp;

	if(target == NULL || filename == NULL)
	{
		return NULL;
	}

	// Process the absolute path specification
	if(StartWith(target,"http://"))
	{
		// Skip http://
		target += 7;

		// Skip the host name portion
		while(*target != '/' && *target != '\0')
		{
			target ++;
		}

		// Error if the "/" isn't included after "http://"
		if(*target == '\0')
		{
			return NULL;
		}

		target++;
	}

	// Unescape
	// (not implemented)

	// Extract the file name portion
	start = target;
	while(*target != '?' && *target != '\0')
	{
		target ++;
	}
	tmp = *target;
	*target = '\0';
	StrCpy(filename, size, start);
	*target = tmp;

	// Interpret if there are parameters
	if(*target == '?')
	{
		LIST *params = NewStrMap();
		UINT i;
		TOKEN_LIST *tl;
		target++;
		tl =ParseToken(target,"&");
		for(i=0;i<tl->NumTokens;i++)
		{
			char *token = tl->Token[i];
			char *body = token;
			STRMAP_ENTRY *newentry = (STRMAP_ENTRY*)Malloc(sizeof(STRMAP_ENTRY));

			while(*body != '=' && *body != '\0')
			{
				*body ++;
			}
			if(*body == '=')
			{
				*body = '\0';
				body++;
			}
			newentry->Name = CopyStr(token);
			newentry->Value = CopyStr(body);
			Add(params, newentry);
//			Debug("PARAMS: %s : %s\n",token,body);
		}
		FreeToken(tl);
		return params;
	}
	return NULL;
}

// Release the parameter list
static void WuFreeStrStrMap(LIST *params)
{
	UINT i;

	if(params == NULL)
	{
		return;
	}

	for(i=0; i<LIST_NUM(params); i++)
	{
		STRMAP_ENTRY *e = (STRMAP_ENTRY*)LIST_DATA(params, i);
		Free(e->Name);
		Free(e->Value);
		Free(e);
	}
	ReleaseList(params);
}

// Read the UTF-8 file and convert as an Unicode string
static wchar_t *WuUniReadFile(char *filename)
{
	IO *io;
	UINT size, usize;
	BYTE *utf8;
	wchar_t *wchars;

	// Validate arguments
	if (filename == NULL)
	{
		return NULL;
	}

	io = FileOpen(filename, false);
	if (io == NULL)
	{
		return NULL;
	}

	// Load the file
	size = FileSize(io);
	utf8 = (BYTE*)Malloc(size);
	FileRead(io, (void*)utf8, size);
	FileClose(io);

	usize = CalcUtf8ToUni(utf8, size);
	wchars = (wchar_t*)ZeroMalloc(usize+sizeof(wchar_t));
	Utf8ToUni(wchars, usize, utf8, size);
	Free(utf8);

	return wchars;
}

// Replace string (with memory reallocation)
static void WuUniReplace(wchar_t **buf, wchar_t *from, wchar_t *to)
{
	UINT dstsize;
	wchar_t *oldbuf = *buf;

	if(buf == NULL || from == NULL || to == NULL)
	{
		return;
	}

	dstsize = (UniCalcReplaceStrEx(*buf, from, to, true) + 1) * sizeof(wchar_t);
	*buf = (wchar_t*)Malloc(dstsize);
	UniReplaceStr(*buf, dstsize, oldbuf, from, to);
	Free(oldbuf);
}

// Insert the string in front of a specified pattern
static void WuUniInsertBefore(wchar_t **buf, wchar_t *insert, wchar_t *before)
{
	UINT tmpsize;
	wchar_t *tmp;

	if(buf == NULL || insert == NULL || before == NULL)
	{
		return;
	}

	tmpsize = (UniStrLen(insert)+UniStrLen(before)+1)*sizeof(wchar_t);
	tmp = (wchar_t*)Malloc(tmpsize);
	UniStrCpy(tmp, tmpsize, insert);
	UniStrCat(tmp, tmpsize, before);
	WuUniReplace(buf, before, tmp);
	Free(tmp);
}

// Uncomment the tag specified by the keyword
static void WuEnableTag(wchar_t **buf, wchar_t *keyword)
{
	wchar_t tmp[MAX_SIZE];
	if(buf == NULL || keyword == NULL)
	{
		return;
	}

	UniFormat(tmp, sizeof(tmp), L"!--%s", keyword);
	WuUniReplace(buf, tmp, L"");

	UniFormat(tmp, sizeof(tmp), L"%s--", keyword);
	WuUniReplace(buf, tmp, L"");
	return;
}

// Generate a session key
static char *WuNewSessionKey()
{
	char tmp[MD5_SIZE], *ret;
	UINT size;
	Rand(tmp, sizeof(tmp));
	size = sizeof(tmp)*2+1;
	ret = Malloc(size);
	BinToStr(ret, size, tmp, sizeof(tmp));
	return ret;
}

// Replace the Unicode pattern in Unicode string with ASCII string
static void WuUniStrReplace(wchar_t **buf, wchar_t *from, char *to)
{
	UINT unisize;
	wchar_t *tmp;

	if(buf == NULL || *buf == NULL || from == NULL || to == NULL)
	{
		return;
	}

	unisize = CalcStrToUni(to);
	tmp = (wchar_t*)Malloc(unisize);
	StrToUni(tmp, unisize, to);
	WuUniReplace(buf, from, tmp);
	Free(tmp);
}

// Extract the template surrounded by specified Unicode string from Unicode string
static wchar_t *WuUniGetTemplate(wchar_t **str, wchar_t *start, wchar_t *end, bool erase)
{
	UINT startidx, endidx, len, size, i;
	wchar_t *ret;

	if(str == NULL || *str == NULL || start == NULL || end == NULL)
	{
		return NULL;
	}

	startidx =	UniSearchStr(*str, start, 0);
	if(startidx == INFINITE)
	{
		return NULL;
	}
	startidx += UniStrLen(start);

	endidx = UniSearchStr(*str, end, startidx);
	if(endidx == INFINITE)
	{
		return NULL;
	}

	len = endidx - startidx;
	size = (len + 1) * sizeof(wchar_t);
	ret = (wchar_t*)Malloc(size);
	for(i=0; i<len; i++)
	{
		ret[i] = (*str)[startidx + i];
	}
	ret[i] = 0;

	if(erase)
	{
		wchar_t tmp[MAX_SIZE*10];
		UniFormat(tmp, sizeof(tmp), L"%s%s%s", start, ret, end);
		WuUniReplace(str, tmp, L"");
	}
	return ret;
}

// Replace the Unicode pattern in the Unicode string with the UINT number
static void WuUniUintReplace(wchar_t **buf, wchar_t *key, UINT num)
{
	wchar_t tmp[MAX_SIZE];
	UniFormat(tmp, sizeof(tmp), L"%d", num);
	WuUniReplace(buf, key, tmp);
}

// Replace the Unicode pattern in the Unicode string with the UINT64 number
static void WuUniUint64Replace(wchar_t **buf, wchar_t *key, UINT64 num)
{
	wchar_t tmp[MAX_SIZE];
	UniFormat(tmp, sizeof(tmp), L"%I64d", num);
	WuUniReplace(buf, key, tmp);
}

// Copy the Unicode string until the appearance of the specified character (escapable with '\')
static wchar_t *WuUniCopyStrTill(wchar_t *str, wchar_t delimiter, wchar_t **ret){
	UINT num = 0, i;
	wchar_t *next = str;
	wchar_t *ptr = str;

	// Count the number of characters to copy
	while(*next)
	{
		if(*next==L'\\')
		{
			next++;
			if(*next == 0)
			{
				break;
			}
		}
		else
		{
			if(*next == delimiter)
			{
				break;
			}
		}
		next++;
		num++;
	}

	// Allocate the memory and copy the string
	*ret = (wchar_t*)Malloc((num+1)*sizeof(wchar_t));
	for(i=0;i<num;i++)
	{
		if(*ptr == L'\\')
		{
			ptr++;
		}
		(*ret)[i] = *ptr;
		ptr++;
	}
	(*ret)[num]=0;

	// Return a pointer to the next to the delimiter or the end of the string
	return *next ? next+1 : next;
}

// Create a string table from Unicode string
static LIST *WuUniMakeTable(wchar_t *def)
{
	LIST *table;
	STRMAP_ENTRY *entry;
	
	if(def==NULL)
	{
		return NULL;
	}

	table = NewStrMap();
	while(*def)
	{
		wchar_t *keytmp;
		UINT keylen;
		entry = (STRMAP_ENTRY*)Malloc(sizeof(STRMAP_ENTRY));
		def = WuUniCopyStrTill(def, L':', &keytmp);
		keylen = CalcUniToStr(keytmp);
		entry->Name = (char*)Malloc(keylen);
		UniToStr(entry->Name, keylen, keytmp);
		def = WuUniCopyStrTill(def, L',', (wchar_t**)&(entry->Value));
		Add(table, entry);
		Free(keytmp);
	}
	return table;
}

// Extract the template from Unicode string, and create a string table from it
static LIST *WuUniMakeTableFromTemplate(wchar_t **str, wchar_t *start, wchar_t *end)
{
	wchar_t *tmpl = WuUniGetTemplate(str, start, end, true);
	LIST *ret = WuUniMakeTable(tmpl);
	Free(tmpl);
	return ret;
}

// Delete the expired session key
static void WuExpireSessionKey(WEBUI *wu)
{
	LIST *Expired = NewList(NULL);
	UINT i;

	LockList(wu->Contexts);

	for(i=0; i<LIST_NUM(wu->Contexts); i++)
	{
		STRMAP_ENTRY *entry = (STRMAP_ENTRY*)LIST_DATA(wu->Contexts, i);
		WU_CONTEXT *context = (WU_CONTEXT*)entry->Value;
		if(context->ExpireDate < Tick64())
		{
			Add(Expired, entry);
		}
	}

	for(i=0; i<LIST_NUM(Expired); i++)
	{
		STRMAP_ENTRY *entry = LIST_DATA(Expired, i); 
		Delete(wu->Contexts, entry);
		Free(entry->Name);
		WuFreeContext(entry->Value);
		Free(entry);
	}
	ReleaseList(Expired);

	UnlockList(wu->Contexts);
}

// Get the context, and extend its expiration date
static WU_CONTEXT *WuGetContext(LIST *contexts, char *sessionkey)
{
	WU_CONTEXT *ret = StrMapSearch(contexts, sessionkey);
	if(ret != NULL)
	{
		ret->ExpireDate = Tick64() + WU_CONTEXT_EXPIRE;
	}
	return ret;
}

// Developed by SoftEther VPN Project at University of Tsukuba in Japan.
// Department of Computer Science has dozens of overly-enthusiastic geeks.
// Join us: http://www.tsukuba.ac.jp/english/admission/
