Index: binary-improvements/MapRendering/Web/API/GetLandClaims.cs
===================================================================
--- binary-improvements/MapRendering/Web/API/GetLandClaims.cs	(revision 242)
+++ binary-improvements/MapRendering/Web/API/GetLandClaims.cs	(revision 244)
@@ -9,5 +9,5 @@
 	public class GetLandClaims : WebAPI
 	{
-		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, HttpListenerBasicIdentity user)
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel)
 		{
 			string steamid = string.Empty;
@@ -17,5 +17,5 @@
 				steamid = req.QueryString ["steamid"];
 				if (steamid.Length != 17 || !long.TryParse (steamid, out tempLong)) {
-					resp.StatusCode = (int)HttpStatusCode.InternalServerError;
+					resp.StatusCode = (int)HttpStatusCode.BadRequest;
 					Web.SetResponseTextContent (resp, "Invalid SteamID given");
 					return;
Index: binary-improvements/MapRendering/Web/API/GetPlayerInventory.cs
===================================================================
--- binary-improvements/MapRendering/Web/API/GetPlayerInventory.cs	(revision 242)
+++ binary-improvements/MapRendering/Web/API/GetPlayerInventory.cs	(revision 244)
@@ -9,5 +9,5 @@
 	public class GetPlayerInventory : WebAPI
 	{
-		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, HttpListenerBasicIdentity user)
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel)
 		{
 			if (req.QueryString ["steamid"] == null) {
Index: binary-improvements/MapRendering/Web/API/GetPlayersLocation.cs
===================================================================
--- binary-improvements/MapRendering/Web/API/GetPlayersLocation.cs	(revision 242)
+++ binary-improvements/MapRendering/Web/API/GetPlayersLocation.cs	(revision 244)
@@ -9,5 +9,5 @@
 	public class GetPlayersLocation : WebAPI
 	{
-		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, HttpListenerBasicIdentity user)
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel)
 		{
 			JSONArray playersJsResult = new JSONArray ();
Index: binary-improvements/MapRendering/Web/API/GetPlayersOnline.cs
===================================================================
--- binary-improvements/MapRendering/Web/API/GetPlayersOnline.cs	(revision 242)
+++ binary-improvements/MapRendering/Web/API/GetPlayersOnline.cs	(revision 244)
@@ -9,5 +9,5 @@
 	public class GetPlayersOnline : WebAPI
 	{
-		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, HttpListenerBasicIdentity user)
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel)
 		{
 			JSONArray players = new JSONArray();
Index: binary-improvements/MapRendering/Web/API/GetStats.cs
===================================================================
--- binary-improvements/MapRendering/Web/API/GetStats.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/API/GetStats.cs	(revision 244)
@@ -0,0 +1,25 @@
+using AllocsFixes.JSON;
+using AllocsFixes.PersistentData;
+using System;
+using System.Collections.Generic;
+using System.Net;
+
+namespace AllocsFixes.NetConnections.Servers.Web.API
+{
+	public class GetStats : WebAPI
+	{
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel)
+		{
+			JSONObject result = new JSONObject ();
+
+			JSONObject time = new JSONObject ();
+			time.Add ("days", new JSONNumber (GameUtils.WorldTimeToDays (GameManager.Instance.World.worldTime)));
+			time.Add ("hours", new JSONNumber (GameUtils.WorldTimeToHours (GameManager.Instance.World.worldTime)));
+			time.Add ("minutes", new JSONNumber (GameUtils.WorldTimeToMinutes (GameManager.Instance.World.worldTime)));
+			result.Add ("gametime", time);
+
+			WriteJSON (resp, result);
+		}
+	}
+}
+
Index: binary-improvements/MapRendering/Web/API/WebAPI.cs
===================================================================
--- binary-improvements/MapRendering/Web/API/WebAPI.cs	(revision 242)
+++ binary-improvements/MapRendering/Web/API/WebAPI.cs	(revision 244)
@@ -16,5 +16,5 @@
 		}
 
-		public abstract void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, HttpListenerBasicIdentity user);
+		public abstract void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel);
 	}
 }
Index: binary-improvements/MapRendering/Web/ApiHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/ApiHandler.cs	(revision 242)
+++ 	(revision )
@@ -1,56 +1,0 @@
-using AllocsFixes.NetConnections.Servers.Web.API;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using System.Threading;
-
-namespace AllocsFixes.NetConnections.Servers.Web
-{
-	public class ApiHandler : PathHandler
-	{
-		private string staticPart;
-		private Dictionary<String, WebAPI> apis = new Dictionary<string, WebAPI> ();
-
-		public ApiHandler (string staticPart)
-		{
-			this.staticPart = staticPart;
-			apis.Add ("getlandclaims", new GetLandClaims ());
-			apis.Add ("getplayersonline", new GetPlayersOnline ());
-			apis.Add ("getplayerslocation", new GetPlayersLocation ());
-			apis.Add ("getplayerinventory", new GetPlayerInventory ());
-		}
-
-		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, HttpListenerBasicIdentity user)
-		{
-			string apiName = req.Url.AbsolutePath.Remove (0, staticPart.Length);
-			if (!AuthorizeForCommand (apiName, user)) {
-				resp.StatusCode = (int)HttpStatusCode.Forbidden;
-			} else {
-				foreach (KeyValuePair<string, WebAPI> kvp in apis) {
-					try {
-						if (apiName.StartsWith (kvp.Key)) {
-							kvp.Value.HandleRequest (req, resp, user);
-							return;
-						}
-					} catch (Exception e) {
-						Log.Out ("Error in ApiHandler.HandleRequest(): Handler threw an exception: " + e);
-						resp.StatusCode = (int)HttpStatusCode.InternalServerError;
-						return;
-					}
-				}
-			}
-	
-			Log.Out ("Error in ApiHandler.HandleRequest(): No handler found for API \"" + apiName + "\"");
-			resp.StatusCode = (int)HttpStatusCode.NotFound;
-		}
-
-		private bool AuthorizeForCommand (string apiName, HttpListenerBasicIdentity user)
-		{
-			return true;
-		}
-
-	}
-
-}
-
Index: binary-improvements/MapRendering/Web/ConnectionHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/ConnectionHandler.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/ConnectionHandler.cs	(revision 244)
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+
+namespace AllocsFixes.NetConnections.Servers.Web
+{
+	public class ConnectionHandler {
+		private Web parent;
+		private Dictionary<string, WebConnection> connections = new Dictionary<string, WebConnection> ();
+
+		public ConnectionHandler (Web _parent) {
+			parent = _parent;
+		}
+
+		public WebConnection IsLoggedIn (string _sessionId, string _endpoint) {
+			if (!connections.ContainsKey (_sessionId)) {
+				return null;
+			}
+
+			WebConnection con = connections [_sessionId];
+//			if (con.Age.TotalMinutes > parent.sessionTimeoutMinutes) {
+//				connections.Remove (_sessionId);
+//				return null;
+//			}
+
+			if (con.Endpoint != _endpoint) {
+				connections.Remove (_sessionId);
+				return null;
+			}
+
+			con.UpdateUsage ();
+
+			return con;
+		}
+
+		public void LogOut (string _sessionId) {
+			connections.Remove (_sessionId);
+		}
+
+		public WebConnection LogIn (ulong _steamId, string _endpoint) {
+			string sessionId = Guid.NewGuid ().ToString ();
+			WebConnection con = new WebConnection (sessionId, _endpoint, _steamId);
+			connections.Add (sessionId, con);
+			return con;
+		}
+
+		public void SendLine (string line) {
+			foreach (WebConnection wc in connections.Values) {
+				wc.SendLine (line);
+			}
+		}
+
+		public void SendLog (string text, string trace, UnityEngine.LogType type) {
+			foreach (WebConnection wc in connections.Values) {
+				wc.SendLog (text, trace, type);
+			}
+		}
+
+	}
+}
+
Index: binary-improvements/MapRendering/Web/Handlers/ApiHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/Handlers/ApiHandler.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/Handlers/ApiHandler.cs	(revision 244)
@@ -0,0 +1,64 @@
+using AllocsFixes.NetConnections.Servers.Web.API;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Threading;
+
+namespace AllocsFixes.NetConnections.Servers.Web.Handlers
+{
+	public class ApiHandler : PathHandler {
+		private string staticPart;
+		private Dictionary<String, WebAPI> apis = new Dictionary<string, WebAPI> ();
+
+		public ApiHandler (string staticPart, string moduleName = null) : base(moduleName) {
+			this.staticPart = staticPart;
+			addApi ("getlandclaims", new GetLandClaims ());
+			addApi ("getplayersonline", new GetPlayersOnline ());
+			addApi ("getplayerslocation", new GetPlayersLocation ());
+			addApi ("getplayerinventory", new GetPlayerInventory ());
+			addApi ("getstats", new GetStats ());
+		}
+
+		private void addApi (string _apiName, WebAPI _api) {
+			apis.Add (_apiName, _api);
+			WebPermissions.Instance.AddKnownModule ("webapi." + _apiName);
+		}
+
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel) {
+			string apiName = req.Url.AbsolutePath.Remove (0, staticPart.Length);
+			if (!AuthorizeForCommand (apiName, user, permissionLevel)) {
+				resp.StatusCode = (int)HttpStatusCode.Forbidden;
+				if (user != null) {
+					Log.Out ("ApiHandler: user '{0}' not allowed to execute '{1}'", user.SteamID, apiName);
+				} else {
+					Log.Out ("ApiHandler: unidentified user from '{0}' not allowed to execute '{1}'", req.RemoteEndPoint.Address, apiName);
+				}
+				return;
+			} else {
+				foreach (KeyValuePair<string, WebAPI> kvp in apis) {
+					try {
+						if (apiName.StartsWith (kvp.Key)) {
+							kvp.Value.HandleRequest (req, resp, user, permissionLevel);
+							return;
+						}
+					} catch (Exception e) {
+						Log.Out ("Error in ApiHandler.HandleRequest(): Handler threw an exception: " + e);
+						resp.StatusCode = (int)HttpStatusCode.InternalServerError;
+						return;
+					}
+				}
+			}
+	
+			Log.Out ("Error in ApiHandler.HandleRequest(): No handler found for API \"" + apiName + "\"");
+			resp.StatusCode = (int)HttpStatusCode.NotFound;
+		}
+
+		private bool AuthorizeForCommand (string apiName, WebConnection user, int permissionLevel) {
+			return WebPermissions.Instance.ModuleAllowedWithLevel ("webapi." + apiName, permissionLevel);
+		}
+
+	}
+
+}
+
Index: binary-improvements/MapRendering/Web/Handlers/ItemIconHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/Handlers/ItemIconHandler.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/Handlers/ItemIconHandler.cs	(revision 244)
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Threading;
+
+using UnityEngine;
+
+namespace AllocsFixes.NetConnections.Servers.Web.Handlers
+{
+	public class ItemIconHandler : PathHandler
+	{
+		private string staticPart;
+		private bool logMissingFiles;
+		private Dictionary<string, byte[]> icons = new Dictionary<string, byte[]> ();
+		private bool loaded = false;
+
+		public ItemIconHandler (string staticPart, bool logMissingFiles, string moduleName = null) : base(moduleName) {
+			this.staticPart = staticPart;
+			this.logMissingFiles = logMissingFiles;
+		}
+
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel) {
+			if (!loaded) {
+				if (!LoadIcons ()) {
+					resp.StatusCode = (int)HttpStatusCode.NotFound;
+					Log.Out ("Web:IconHandler: Could not load icons");
+					return;
+				}
+			}
+
+			string fn = req.Url.AbsolutePath.Remove (0, staticPart.Length);
+			fn = fn.Remove (fn.LastIndexOf ('.'));
+
+			if (icons.ContainsKey (fn)) {
+				resp.ContentType = MimeType.GetMimeType (".png");
+				resp.ContentLength64 = icons [fn].Length;
+				resp.OutputStream.Write (icons [fn], 0, icons [fn].Length);
+			} else {
+				resp.StatusCode = (int)HttpStatusCode.NotFound;
+				if (logMissingFiles)
+					Log.Out ("Web:IconHandler:FileNotFound: \"" + req.Url.AbsolutePath + "\" ");
+				return;
+			}
+		}
+
+		private bool LoadIcons () {
+			lock (icons) {
+				if (loaded) {
+					return true;
+				}
+
+				GameObject atlasObj = GameObject.Find ("/NGUI Root (2D)/ItemIconAtlas");
+				if (atlasObj == null) {
+					Log.Error ("Web:IconHandler: Atlas object not found");
+					loaded = true;
+					return false;
+				}
+				DynamicUIAtlas atlas = atlasObj.GetComponent<DynamicUIAtlas> ();
+				if (atlas == null) {
+					Log.Error ("Web:IconHandler: Atlas component not found");
+					loaded = true;
+					return false;
+				}
+
+				Texture2D atlasTex = atlas.texture as Texture2D;
+
+				foreach (UISpriteData data in atlas.spriteList) {
+					string name = data.name;
+					Texture2D tex = new Texture2D (data.width, data.height, TextureFormat.ARGB32, false);
+					tex.SetPixels (atlasTex.GetPixels (data.x, atlasTex.height - data.height - data.y, data.width, data.height));
+					byte[] pixData = tex.EncodeToPNG ();
+
+					icons.Add (name, pixData);
+					UnityEngine.Object.Destroy (tex);
+				}
+
+				loaded = true;
+				Log.Out ("Web:IconHandler: Icons loaded");
+
+				return true;
+			}
+		}
+	}
+}
+
Index: binary-improvements/MapRendering/Web/Handlers/PathHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/Handlers/PathHandler.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/Handlers/PathHandler.cs	(revision 244)
@@ -0,0 +1,29 @@
+using System;
+using System.Net;
+
+namespace AllocsFixes.NetConnections.Servers.Web.Handlers
+{
+	public abstract class PathHandler
+	{
+		private string moduleName = null;
+		public string ModuleName {
+			get { return moduleName; }
+		}
+
+		protected PathHandler (string _moduleName) {
+			this.moduleName = _moduleName;
+			WebPermissions.Instance.AddKnownModule (_moduleName);
+		}
+
+		public abstract void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel);
+
+		public bool IsAuthorizedForHandler (WebConnection user, int permissionLevel) {
+			if (moduleName != null) {
+				return WebPermissions.Instance.ModuleAllowedWithLevel (moduleName, permissionLevel);
+			} else {
+				return true;
+			}
+		}
+	}
+}
+
Index: binary-improvements/MapRendering/Web/Handlers/SessionHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/Handlers/SessionHandler.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/Handlers/SessionHandler.cs	(revision 244)
@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Threading;
+
+namespace AllocsFixes.NetConnections.Servers.Web.Handlers
+{
+	public class SessionHandler : PathHandler {
+		private string staticPart;
+		private Web parent;
+		private string header = "";
+		private string footer = "";
+
+		public SessionHandler (string _staticPart, string _dataFolder, Web _parent, string moduleName = null) : base(moduleName) {
+			this.staticPart = _staticPart;
+			this.parent = _parent;
+
+			if (File.Exists (_dataFolder + "/sessionheader.tmpl")) {
+				header = File.ReadAllText (_dataFolder + "/sessionheader.tmpl");
+			}
+
+			if (File.Exists (_dataFolder + "/sessionfooter.tmpl")) {
+				footer = File.ReadAllText (_dataFolder + "/sessionfooter.tmpl");
+			}
+		}
+
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel) {
+			string subpath = req.Url.AbsolutePath.Remove (0, staticPart.Length);
+
+			StringBuilder result = new StringBuilder ();
+			result.Append (header);
+
+			if (subpath.StartsWith ("verify")) {
+				if (user != null) {
+					resp.Redirect ("/static/index.html");
+					return;
+				} else {
+					result.Append ("<h1>Login failed, <a href=\"/static/index.html\">click to return to main page</a>.</h1>");
+				}
+			} else if (subpath.StartsWith ("logout")) {
+				if (user != null) {
+					parent.connectionHandler.LogOut (user.SessionID);
+					Cookie cookie = new Cookie ("sid", "", "/");
+					cookie.Expired = true;
+					resp.AppendCookie (cookie);
+					resp.Redirect ("/static/index.html");
+					return;
+				} else {
+					result.Append ("<h1>Not logged in, <a href=\"/static/index.html\">click to return to main page</a>.</h1>");
+				}
+			} else if (subpath.StartsWith ("login")) {
+				string host = (parent.isSslRedirected ? "https://" : "http://") + req.UserHostName;
+				string url = OpenID.GetOpenIdLoginUrl (host, host + "/session/verify");
+				resp.Redirect (url);
+				return;
+			} else {
+				result.Append ("<h1>Unknown command, <a href=\"/static/index.html\">click to return to main page</a>.</h1>");
+			}
+
+			result.Append (footer);
+
+			resp.ContentType = MimeType.GetMimeType (".html");
+			resp.ContentEncoding = Encoding.UTF8;
+			byte[] buf = Encoding.UTF8.GetBytes (result.ToString ());
+			resp.ContentLength64 = buf.Length;
+			resp.OutputStream.Write (buf, 0, buf.Length);
+		}
+
+	}
+
+}
+
Index: binary-improvements/MapRendering/Web/Handlers/SimpleRedirectHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/Handlers/SimpleRedirectHandler.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/Handlers/SimpleRedirectHandler.cs	(revision 244)
@@ -0,0 +1,21 @@
+using System;
+using System.Net;
+
+namespace AllocsFixes.NetConnections.Servers.Web.Handlers
+{
+	public class SimpleRedirectHandler : PathHandler
+	{
+		string target;
+
+		public SimpleRedirectHandler (string target, string moduleName = null) : base(moduleName)
+		{
+			this.target = target;
+		}
+
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel)
+		{
+			resp.Redirect (target);
+		}
+	}
+}
+
Index: binary-improvements/MapRendering/Web/Handlers/StaticHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/Handlers/StaticHandler.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/Handlers/StaticHandler.cs	(revision 244)
@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Threading;
+
+namespace AllocsFixes.NetConnections.Servers.Web.Handlers
+{
+	public class StaticHandler : PathHandler
+	{
+		private string datapath;
+		private string staticPart;
+		private AllocsFixes.FileCache.AbstractCache cache;
+		private bool logMissingFiles;
+
+		public StaticHandler (string staticPart, string filePath, AllocsFixes.FileCache.AbstractCache cache, bool logMissingFiles, string moduleName = null) : base(moduleName)
+		{
+			this.staticPart = staticPart;
+			this.datapath = filePath;
+			this.cache = cache;
+			this.logMissingFiles = logMissingFiles;
+		}
+
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel)
+		{
+			string fn = req.Url.AbsolutePath.Remove (0, staticPart.Length);
+
+			byte[] content = cache.GetFileContent (datapath + "/" + fn);
+			if (content != null) {
+				resp.ContentType = MimeType.GetMimeType (Path.GetExtension (fn));
+				resp.ContentLength64 = content.Length;
+				resp.OutputStream.Write (content, 0, content.Length);
+			} else {
+				resp.StatusCode = (int)HttpStatusCode.NotFound;
+				if (logMissingFiles)
+					Log.Out ("Web:Static:FileNotFound: \"" + req.Url.AbsolutePath + "\" @ \"" + datapath + "/" + req.Url.AbsolutePath.Remove (0, staticPart.Length) + "\"");
+				return;
+			}
+		}
+	}
+}
+
Index: binary-improvements/MapRendering/Web/Handlers/UserStatusHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/Handlers/UserStatusHandler.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/Handlers/UserStatusHandler.cs	(revision 244)
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Threading;
+using System.Text;
+using AllocsFixes.JSON;
+
+namespace AllocsFixes.NetConnections.Servers.Web.Handlers
+{
+	public class UserStatusHandler : PathHandler {
+		public UserStatusHandler (string moduleName = null) : base(moduleName) {
+		}
+
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel) {
+			JSONObject result = new JSONObject ();
+
+			result.Add ("loggedin", new JSONBoolean (user != null));
+			result.Add ("username", new JSONString (user != null ? user.SteamID.ToString () : string.Empty));
+
+			JSONArray perms = new JSONArray ();
+			foreach (WebPermissions.WebModulePermission perm in WebPermissions.Instance.GetModules ()) {
+				JSONObject permObj = new JSONObject ();
+				permObj.Add ("module", new JSONString (perm.module));
+				permObj.Add ("allowed", new JSONBoolean (WebPermissions.Instance.ModuleAllowedWithLevel (perm.module, permissionLevel)));
+				perms.Add (permObj);
+			}
+			result.Add ("permissions", perms);
+
+			WriteJSON (resp, result);
+		}
+
+		public void WriteJSON (HttpListenerResponse resp, JSONNode root) {
+			byte[] buf = Encoding.UTF8.GetBytes (root.ToString ());
+			resp.ContentLength64 = buf.Length;
+			resp.ContentType = "application/json";
+			resp.ContentEncoding = Encoding.UTF8;
+			resp.OutputStream.Write (buf, 0, buf.Length);
+		}
+
+	}
+
+}
+
Index: binary-improvements/MapRendering/Web/ItemIconHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/ItemIconHandler.cs	(revision 242)
+++ 	(revision )
@@ -1,86 +1,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using System.Threading;
-
-using UnityEngine;
-
-namespace AllocsFixes.NetConnections.Servers.Web
-{
-	public class ItemIconHandler : PathHandler
-	{
-		private string staticPart;
-		private bool logMissingFiles;
-		private Dictionary<string, byte[]> icons = new Dictionary<string, byte[]> ();
-		private bool loaded = false;
-
-		public ItemIconHandler (string staticPart, bool logMissingFiles) {
-			this.staticPart = staticPart;
-			this.logMissingFiles = logMissingFiles;
-		}
-
-		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, HttpListenerBasicIdentity user) {
-			if (!loaded) {
-				if (!LoadIcons ()) {
-					resp.StatusCode = (int)HttpStatusCode.NotFound;
-					Log.Out ("Web:IconHandler: Could not load icons");
-					return;
-				}
-			}
-
-			string fn = req.Url.AbsolutePath.Remove (0, staticPart.Length);
-			fn = fn.Remove (fn.LastIndexOf ('.'));
-
-			if (icons.ContainsKey (fn)) {
-				resp.ContentType = MimeType.GetMimeType (".png");
-				resp.ContentLength64 = icons [fn].Length;
-				resp.OutputStream.Write (icons [fn], 0, icons [fn].Length);
-			} else {
-				resp.StatusCode = (int)HttpStatusCode.NotFound;
-				if (logMissingFiles)
-					Log.Out ("Web:IconHandler:FileNotFound: \"" + req.Url.AbsolutePath + "\" ");
-				return;
-			}
-		}
-
-		private bool LoadIcons () {
-			lock (icons) {
-				if (loaded) {
-					return true;
-				}
-
-				GameObject atlasObj = GameObject.Find ("/NGUI Root (2D)/ItemIconAtlas");
-				if (atlasObj == null) {
-					Log.Error ("Web:IconHandler: Atlas object not found");
-					loaded = true;
-					return false;
-				}
-				DynamicUIAtlas atlas = atlasObj.GetComponent<DynamicUIAtlas> ();
-				if (atlas == null) {
-					Log.Error ("Web:IconHandler: Atlas component not found");
-					loaded = true;
-					return false;
-				}
-
-				Texture2D atlasTex = atlas.texture as Texture2D;
-
-				foreach (UISpriteData data in atlas.spriteList) {
-					string name = data.name;
-					Texture2D tex = new Texture2D (data.width, data.height, TextureFormat.ARGB32, false);
-					tex.SetPixels (atlasTex.GetPixels (data.x, atlasTex.height - data.height - data.y, data.width, data.height));
-					byte[] pixData = tex.EncodeToPNG ();
-
-					icons.Add (name, pixData);
-					UnityEngine.Object.Destroy (tex);
-				}
-
-				loaded = true;
-				Log.Out ("Web:IconHandler: Icons loaded");
-
-				return true;
-			}
-		}
-	}
-}
-
Index: binary-improvements/MapRendering/Web/OpenID.cs
===================================================================
--- binary-improvements/MapRendering/Web/OpenID.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/OpenID.cs	(revision 244)
@@ -0,0 +1,118 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.IO;
+using System.Net;
+using System.Net.Security;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace AllocsFixes.NetConnections.Servers.Web
+{
+	public static class OpenID {
+		private const string STEAM_LOGIN = "https://steamcommunity.com/openid/login";
+		private static Regex steamIdUrlMatcher = new Regex (@"^http:\/\/steamcommunity\.com\/openid\/id\/([0-9]{17,18})");
+
+		static OpenID () {
+			ServicePointManager.ServerCertificateValidationCallback = (srvPoint, certificate, chain, errors) => {
+				if (errors == SslPolicyErrors.None)
+					return true;
+
+				Log.Out ("Steam certificate error: {0}", errors);
+
+				return true;
+			};
+
+		}
+
+		public static string GetOpenIdLoginUrl (string _returnHost, string _returnUrl) {
+			Dictionary<string, string> queryParams = new Dictionary<string, string> ();
+
+			queryParams.Add ("openid.ns", "http://specs.openid.net/auth/2.0");
+			queryParams.Add ("openid.mode", "checkid_setup");
+			queryParams.Add ("openid.return_to", _returnUrl);
+			queryParams.Add ("openid.realm", _returnHost);
+			queryParams.Add ("openid.identity", "http://specs.openid.net/auth/2.0/identifier_select");
+			queryParams.Add ("openid.claimed_id", "http://specs.openid.net/auth/2.0/identifier_select");
+
+			return STEAM_LOGIN + '?' + buildUrlParams (queryParams);
+		}
+
+		public static ulong Validate (HttpListenerRequest _req) {
+			if (getValue (_req, "openid.mode") == "cancel") {
+				Log.Warning ("Steam OpenID login canceled");
+				return 0;
+			}
+			string steamIdString = getValue (_req, "openid.claimed_id");
+			ulong steamId = 0;
+			Match steamIdMatch = steamIdUrlMatcher.Match (steamIdString);
+			if (steamIdMatch.Success) {
+				steamId = ulong.Parse (steamIdMatch.Groups [1].Value);
+			} else {
+				Log.Warning ("Steam OpenID login result did not give a valid SteamID");
+				return 0;
+			}
+
+			Dictionary<string, string> queryParams = new Dictionary<string, string> ();
+
+			queryParams.Add ("openid.ns", "http://specs.openid.net/auth/2.0");
+
+			queryParams.Add ("openid.assoc_handle", getValue (_req, "openid.assoc_handle"));
+			queryParams.Add ("openid.signed", getValue (_req, "openid.signed"));
+			queryParams.Add ("openid.sig", getValue (_req, "openid.sig"));
+			queryParams.Add ("openid.identity", "http://specs.openid.net/auth/2.0/identifier_select");
+			queryParams.Add ("openid.claimed_id", "http://specs.openid.net/auth/2.0/identifier_select");
+
+			string[] signeds = getValue (_req, "openid.signed").Split (',');
+			foreach (string s in signeds) {
+				queryParams ["openid." + s] = getValue (_req, "openid." + s);
+			}
+
+			queryParams.Add ("openid.mode", "check_authentication");
+
+			byte[] postData = Encoding.ASCII.GetBytes (buildUrlParams (queryParams));
+			HttpWebRequest request = (HttpWebRequest)WebRequest.Create (STEAM_LOGIN);
+			request.Method = "POST";
+			request.ContentType = "application/x-www-form-urlencoded";
+			request.ContentLength = postData.Length;
+			request.Headers.Add (HttpRequestHeader.AcceptLanguage, "en");
+			using (Stream st = request.GetRequestStream ()) {
+				st.Write (postData, 0, postData.Length);
+			}
+
+			HttpWebResponse response = (HttpWebResponse)request.GetResponse ();
+			string responseString = null;
+			using (Stream st = response.GetResponseStream ()) {
+				using (StreamReader str = new StreamReader (st)) {
+					responseString = str.ReadToEnd ();
+				}
+			}
+
+			if (responseString.ToLower ().Contains ("is_valid:true")) {
+				return steamId;
+			} else {
+				Log.Warning ("Steam OpenID login failed: {0}", responseString);
+				return 0;
+			}
+		}
+
+		private static string buildUrlParams (Dictionary<string, string> _queryParams) {
+			string[] paramsArr = new string[_queryParams.Count];
+			int i = 0;
+			foreach (KeyValuePair<string, string> kvp in _queryParams) {
+				paramsArr [i++] = kvp.Key + "=" + Uri.EscapeDataString (kvp.Value);
+			}
+			return string.Join ("&", paramsArr);
+		}
+
+		private static string getValue (HttpListenerRequest _req, string _name) {
+			NameValueCollection nvc = _req.QueryString;
+			if (nvc [_name] == null) {
+				throw new MissingMemberException ("OpenID parameter \"" + _name + "\" missing");
+			}
+			return nvc [_name];
+		}
+
+	}
+}
+
Index: binary-improvements/MapRendering/Web/PathHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/PathHandler.cs	(revision 242)
+++ 	(revision )
@@ -1,11 +1,0 @@
-using System;
-using System.Net;
-
-namespace AllocsFixes.NetConnections.Servers.Web
-{
-	public abstract class PathHandler
-	{
-		public abstract void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, HttpListenerBasicIdentity user);
-	}
-}
-
Index: binary-improvements/MapRendering/Web/SimpleRedirectHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/SimpleRedirectHandler.cs	(revision 242)
+++ 	(revision )
@@ -1,21 +1,0 @@
-using System;
-using System.Net;
-
-namespace AllocsFixes.NetConnections.Servers.Web
-{
-	public class SimpleRedirectHandler : PathHandler
-	{
-		string target;
-
-		public SimpleRedirectHandler (string target)
-		{
-			this.target = target;
-		}
-
-		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, HttpListenerBasicIdentity user)
-		{
-			resp.Redirect (target);
-		}
-	}
-}
-
Index: binary-improvements/MapRendering/Web/StaticHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/StaticHandler.cs	(revision 242)
+++ 	(revision )
@@ -1,42 +1,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using System.Threading;
-
-namespace AllocsFixes.NetConnections.Servers.Web
-{
-	public class StaticHandler : PathHandler
-	{
-		private string datapath;
-		private string staticPart;
-		private AllocsFixes.FileCache.AbstractCache cache;
-		private bool logMissingFiles;
-
-		public StaticHandler (string staticPart, string filePath, AllocsFixes.FileCache.AbstractCache cache, bool logMissingFiles)
-		{
-			this.staticPart = staticPart;
-			this.datapath = filePath;
-			this.cache = cache;
-			this.logMissingFiles = logMissingFiles;
-		}
-
-		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, HttpListenerBasicIdentity user)
-		{
-			string fn = req.Url.AbsolutePath.Remove (0, staticPart.Length);
-
-			byte[] content = cache.GetFileContent (datapath + "/" + fn);
-			if (content != null) {
-				resp.ContentType = MimeType.GetMimeType (Path.GetExtension (fn));
-				resp.ContentLength64 = content.Length;
-				resp.OutputStream.Write (content, 0, content.Length);
-			} else {
-				resp.StatusCode = (int)HttpStatusCode.NotFound;
-				if (logMissingFiles)
-					Log.Out ("Web:Static:FileNotFound: \"" + req.Url.AbsolutePath + "\" @ \"" + datapath + "/" + req.Url.AbsolutePath.Remove (0, staticPart.Length) + "\"");
-				return;
-			}
-		}
-	}
-}
-
Index: binary-improvements/MapRendering/Web/Web.cs
===================================================================
--- binary-improvements/MapRendering/Web/Web.cs	(revision 242)
+++ binary-improvements/MapRendering/Web/Web.cs	(revision 244)
@@ -1,4 +1,5 @@
 using System;
 using System.Collections.Generic;
+using System.Collections.Specialized;
 using System.IO;
 using System.Net;
@@ -9,15 +10,23 @@
 using UnityEngine;
 
+using AllocsFixes.NetConnections.Servers.Web.Handlers;
+
 namespace AllocsFixes.NetConnections.Servers.Web
 {
 	public class Web : IConsoleServer {
+		private const int GUEST_PERMISSION_LEVEL = 2000;
 		private readonly HttpListener _listener = new HttpListener ();
 		private Dictionary<string, PathHandler> handlers = new Dictionary<string, PathHandler> ();
-		private bool authEnabled = false;
-		private string realm = "7dtd Admin Panel";
 		public static int handlingCount = 0;
 		public static int currentHandlers = 0;
 		private string dataFolder;
-		private bool mapEnabled = false;
+		private bool useStaticCache = false;
+
+		public bool isSslRedirected {
+			private set;
+			get;
+		}
+
+		public ConnectionHandler connectionHandler;
 
 		public Web () {
@@ -25,5 +34,5 @@
 				int webPort = GamePrefs.GetInt (EnumGamePrefs.ControlPanelPort);
 				if (webPort < 1 || webPort > 65533) {
-					Log.Out ("Webserver not started (ControlPanelPort not within 1-65534)");
+					Log.Out ("Webserver not started (ControlPanelPort not within 1-65533)");
 					return;
 				}
@@ -33,4 +42,8 @@
 				}
 
+				// TODO: Read from config
+				isSslRedirected = false;
+				useStaticCache = false;
+
 				dataFolder = Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location) + "/webserver";
 
@@ -47,11 +60,33 @@
 						new SimpleRedirectHandler ("/static/favicon.ico"));
 				handlers.Add (
-						"/static/",
-						new StaticHandler (
-								"/static/",
-								dataFolder,
-								new AllocsFixes.FileCache.DirectAccess (),
-								true)
-				); // TODO: Enable cache
+						"/session/",
+						new SessionHandler (
+									"/session/",
+									dataFolder,
+									this)
+				);
+				handlers.Add (
+						"/userstatus",
+						new UserStatusHandler ()
+				);
+				if (useStaticCache) {
+					handlers.Add (
+							"/static/",
+							new StaticHandler (
+									"/static/",
+									dataFolder,
+									new AllocsFixes.FileCache.SimpleCache (),
+									true)
+					);
+				} else {
+					handlers.Add (
+							"/static/",
+							new StaticHandler (
+									"/static/",
+									dataFolder,
+									new AllocsFixes.FileCache.DirectAccess (),
+									true)
+					);
+				}
 
 				handlers.Add (
@@ -68,16 +103,17 @@
 						GameUtils.GetSaveGameDir () + "/map",
 						MapRendering.MapRendering.GetTileCache (),
-						false)
-				);
-
-				handlers.Add ("/api/", new ApiHandler ("/api/"));
+						false,
+						"web.map")
+				);
+
+				handlers.Add (
+					"/api/",
+					new ApiHandler ("/api/")
+				);
+
+				connectionHandler = new ConnectionHandler (this);
 
 				_listener.Prefixes.Add (String.Format ("http://*:{0}/", webPort + 2));
-				authEnabled = File.Exists (dataFolder + "/protect");
-				if (authEnabled) {
-					_listener.AuthenticationSchemes = AuthenticationSchemes.Basic;
-				}
 				_listener.Start ();
-				_listener.Realm = realm;
 
 				SdtdConsole.Instance.RegisterServer (this);
@@ -85,5 +121,5 @@
 				_listener.BeginGetContext (new AsyncCallback (HandleRequest), _listener);
 
-				Log.Out ("Started Webserver on " + (webPort + 2) + " (authentication " + (authEnabled ? "enabled" : "disabled") + ")");
+				Log.Out ("Started Webserver on " + (webPort + 2));
 			} catch (Exception e) {
 				Log.Out ("Error in Web.ctor: " + e);
@@ -98,27 +134,48 @@
 				_listener.BeginGetContext (new AsyncCallback (HandleRequest), _listener);
 				try {
-					ctx.Response.ProtocolVersion = new Version ("1.0");
-
-					HttpListenerBasicIdentity user = Authorize (ctx);
-
-					if (!authEnabled || (user.Name.ToLower ().Equals ("admin") && user.Password.Equals (GamePrefs.GetString (EnumGamePrefs.ControlPanelPassword)))) {
-						if (ctx.Request.Url.AbsolutePath.Length < 2) {
-							handlers ["/index.htm"].HandleRequest (ctx.Request, ctx.Response, user);
-							return;
-						} else {
-							foreach (KeyValuePair<string, PathHandler> kvp in handlers) {
-								if (ctx.Request.Url.AbsolutePath.StartsWith (kvp.Key)) {
-									kvp.Value.HandleRequest (ctx.Request, ctx.Response, user);
-									return;
+					HttpListenerRequest request = ctx.Request;
+					HttpListenerResponse response = ctx.Response;
+
+					response.ProtocolVersion = new Version ("1.1");
+
+					WebConnection conn;
+					int permissionLevel = DoAuthentication (request, out conn);
+
+
+					//Log.Out ("Login status: conn!=null: {0}, permissionlevel: {1}", conn != null, permissionLevel);
+
+
+					if (conn != null) {
+						Cookie cookie = new Cookie ("sid", conn.SessionID, "/");
+						cookie.Expired = false;
+						cookie.Expires = new DateTime (2020, 1, 1);
+						cookie.HttpOnly = true;
+						cookie.Secure = false;
+						response.AppendCookie (cookie);
+					}
+
+					if (request.Url.AbsolutePath.Length < 2) {
+						handlers ["/index.htm"].HandleRequest (request, response, conn, permissionLevel);
+						return;
+					} else {
+						foreach (KeyValuePair<string, PathHandler> kvp in handlers) {
+							if (request.Url.AbsolutePath.StartsWith (kvp.Key)) {
+								if (!kvp.Value.IsAuthorizedForHandler (conn, permissionLevel)) {
+									response.StatusCode = (int)HttpStatusCode.Forbidden;
+									if (conn != null) {
+										Log.Out ("Web.HandleRequest: user '{0}' not allowed to access '{1}'", conn.SteamID, kvp.Value.ModuleName);
+									} else {
+										Log.Out ("Web.HandleRequest: unidentified user from '{0}' not allowed to access '{1}'", request.RemoteEndPoint.Address, kvp.Value.ModuleName);
+									}
+								} else {
+									kvp.Value.HandleRequest (request, response, conn, permissionLevel);
 								}
+								return;
 							}
 						}
-
-						Log.Out ("Error in Web.HandleRequest(): No handler found for path \"" + ctx.Request.Url.AbsolutePath + "\"");
-						ctx.Response.StatusCode = (int)HttpStatusCode.NotFound;
-					} else {
-						ctx.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
-						ctx.Response.Headers ["WWW-Authenticate"] = "Basic realm=\"" + realm + "\"";
-					}
+					}
+
+					Log.Out ("Error in Web.HandleRequest(): No handler found for path \"" + request.Url.AbsolutePath + "\"");
+					response.StatusCode = (int)HttpStatusCode.NotFound;
 				} catch (IOException e) {
 					if (e.InnerException is SocketException) {
@@ -131,5 +188,5 @@
 				} finally {
 					if (ctx != null) {
-						ctx.Response.OutputStream.Close ();
+						ctx.Response.Close ();
 					}
 					Interlocked.Decrement (ref currentHandlers);
@@ -138,10 +195,42 @@
 		}
 
-		private HttpListenerBasicIdentity Authorize (HttpListenerContext ctx) {
-			try {
-				return (HttpListenerBasicIdentity)ctx.User.Identity;
-			} catch (NullReferenceException) {
-				return null;
-			}
+		private int DoAuthentication (HttpListenerRequest _req, out WebConnection _con) {
+			_con = null;
+
+			string sessionId = null;
+			if (_req.Cookies ["sid"] != null) {
+				sessionId = _req.Cookies ["sid"].Value;
+			}
+
+			if (!string.IsNullOrEmpty (sessionId)) {
+				WebConnection con = connectionHandler.IsLoggedIn (sessionId, _req.RemoteEndPoint.Address.ToString ());
+				if (con != null) {
+					_con = con;
+					return GameManager.Instance.adminTools.GetAdminToolsClientInfo (_con.SteamID.ToString ()).PermissionLevel;
+				}
+			}
+
+			if (_req.QueryString ["adminuser"] != null && _req.QueryString ["admintoken"] != null) {
+				WebPermissions.AdminToken admin = WebPermissions.Instance.GetWebAdmin (_req.QueryString ["adminuser"], _req.QueryString ["admintoken"]);
+				if (admin != null) {
+					return admin.permissionLevel;
+				} else {
+					Log.Warning ("Invalid Admintoken used from " + _req.RemoteEndPoint.ToString ());
+				}
+			}
+
+			if (_req.Url.AbsolutePath.StartsWith ("/session/verify")) {
+				ulong id = OpenID.Validate (_req);
+				if (id > 0) {
+					WebConnection con = connectionHandler.LogIn (id, _req.RemoteEndPoint.Address.ToString ());
+					_con = con;
+					//Log.Out ("Logged in with session id: {0}", con.SessionID);
+					return GameManager.Instance.adminTools.GetAdminToolsClientInfo (id.ToString ()).PermissionLevel;
+				} else {
+					Log.Out ("Steam OpenID login failed from {0}", _req.RemoteEndPoint.ToString ());
+				}
+			}
+
+			return GUEST_PERMISSION_LEVEL;
 		}
 
@@ -156,13 +245,9 @@
 
 		public void SendLine (string line) {
-			try {
-				//Log.Out ("NOT IMPLEMENTED: Web.WriteToClient");
-			} catch (Exception e) {
-				Log.Out ("Error in Web.WriteToClient: " + e);
-			}
+			connectionHandler.SendLine (line);
 		}
 
 		public void SendLog (string text, string trace, UnityEngine.LogType type) {
-			//throw new System.NotImplementedException ();
+			connectionHandler.SendLog (text, trace, type);
 		}
 
Index: binary-improvements/MapRendering/Web/WebConnection.cs
===================================================================
--- binary-improvements/MapRendering/Web/WebConnection.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/WebConnection.cs	(revision 244)
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+
+using UnityEngine;
+
+namespace AllocsFixes.NetConnections.Servers.Web
+{
+	public class WebConnection : ConsoleConnectionAbstract {
+		private string sessionId;
+		private string endpoint;
+		private ulong steamId;
+		private DateTime login;
+		private DateTime lastAction;
+
+		private List<string> outputLines = new List<string> ();
+		private List<LogLine> logLines = new List<LogLine> ();
+
+		public string SessionID {
+			get { return sessionId; }
+		}
+
+		public string Endpoint {
+			get { return endpoint; }
+		}
+
+		public ulong SteamID {
+			get { return steamId; }
+		}
+
+		public TimeSpan Age {
+			get { return DateTime.Now - lastAction; }
+		}
+
+		public WebConnection (string _sessionId, string _endpoint, ulong _steamId) {
+			this.sessionId = _sessionId;
+			this.endpoint = _endpoint;
+			this.steamId = _steamId;
+			this.login = DateTime.Now;
+			this.lastAction = this.login;
+		}
+
+		public void UpdateUsage () {
+			this.lastAction = DateTime.Now;
+		}
+
+		public override string GetDescription () {
+			return "WebPanel from " + endpoint;
+		}
+
+		public override void SendLine (string _text) {
+			outputLines.Add (_text);
+		}
+
+		public override void SendLines (List<string> _output) {
+			outputLines.AddRange (_output);
+		}
+
+		public override void SendLog (string _msg, string _trace, LogType _type) {
+			LogLine ll = new LogLine ();
+			ll.message = _msg;
+			ll.trace = _trace;
+			ll.type = _type;
+			logLines.Add (ll);
+		}
+
+		private struct LogLine {
+			public string message;
+			public string trace;
+			public LogType type;
+		}
+	}
+}
+
Index: binary-improvements/MapRendering/Web/WebPermissions.cs
===================================================================
--- binary-improvements/MapRendering/Web/WebPermissions.cs	(revision 244)
+++ binary-improvements/MapRendering/Web/WebPermissions.cs	(revision 244)
@@ -0,0 +1,325 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+
+namespace AllocsFixes.NetConnections.Servers.Web
+{
+	public class WebPermissions {
+		private static WebPermissions instance = null;
+
+		public static WebPermissions Instance {
+			get {
+				lock (typeof(WebPermissions)) {
+					if (instance == null) {
+						instance = new WebPermissions ();
+					}
+					return instance;
+				}
+			}
+		}
+
+		private const string PERMISSIONS_FILE = "webpermissions.xml";
+		Dictionary<string, AdminToken> admintokens;
+		Dictionary<string, WebModulePermission> modules;
+		Dictionary<string, WebModulePermission> knownModules = new Dictionary<string, WebModulePermission> ();
+		WebModulePermission defaultModulePermission = new WebModulePermission ("", 0);
+		FileSystemWatcher fileWatcher;
+
+		public WebPermissions () {
+			Directory.CreateDirectory (GetFilePath ());
+			InitFileWatcher ();
+			Load ();
+		}
+
+		public bool ModuleAllowedWithLevel (string _module, int _level) {
+			WebModulePermission permInfo = GetModulePermission (_module);
+			return permInfo.permissionLevel >= _level;
+		}
+
+		public AdminToken GetWebAdmin (string _name, string _token) {
+			if (IsAdmin (_name) && admintokens [_name].token == _token) {
+				return admintokens [_name];
+			} else {
+				return null;
+			}
+		}
+
+		public WebModulePermission GetModulePermission (string _module) {
+			if (modules.ContainsKey (_module.ToLower ())) {
+				return modules [_module.ToLower ()];
+			}
+			return defaultModulePermission;
+		}
+
+
+		// Admins
+		public void AddAdmin (string _name, string _token, int _permissionLevel, bool _save = true) {
+			AdminToken c = new AdminToken (_name, _token, _permissionLevel);
+			lock (this) {
+				admintokens [_name] = c;
+				if (_save) {
+					Save ();
+				}
+			}
+		}
+
+		public void RemoveAdmin (string _name, bool _save = true) {
+			lock (this) {
+				admintokens.Remove (_name);
+				if (_save) {
+					Save ();
+				}
+			}
+		}
+
+		public bool IsAdmin (string _name) {
+			return admintokens.ContainsKey (_name);
+		}
+
+		public AdminToken[] GetAdmins () {
+			AdminToken[] result = new AdminToken[admintokens.Count];
+			admintokens.Values.CopyTo (result, 0);
+			return result;
+		}
+	
+
+		// Commands
+		public void AddModulePermission (string _module, int _permissionLevel, bool _save = true) {
+			WebModulePermission p = new WebModulePermission (_module.ToLower (), _permissionLevel);
+			lock (this) {
+				modules [_module] = p;
+				if (_save) {
+					Save ();
+				}
+			}
+		}
+
+		public void AddKnownModule (string _module) {
+			if (!string.IsNullOrEmpty (_module)) {
+				lock (this) {
+					knownModules.Add (_module, new WebModulePermission (_module, 0));
+				}
+			}
+		}
+
+		public bool IsKnownModule (string _module) {
+			if (!string.IsNullOrEmpty (_module)) {
+				lock (this) {
+					return knownModules.ContainsKey (_module);
+				}
+			} else {
+				return false;
+			}
+		}
+	
+		public void RemoveModulePermission (string _module, bool _save = true) {
+			lock (this) {
+				modules.Remove (_module.ToLower ());
+				if (_save) {
+					Save ();
+				}
+			}
+		}
+
+		public List<WebModulePermission> GetModules () {
+			List<WebModulePermission> result = new List<WebModulePermission> ();
+			foreach (string module in knownModules.Keys) {
+				if (modules.ContainsKey (module)) {
+					result.Add (modules [module]);
+				} else {
+					result.Add (knownModules [module]);
+				}
+			}
+
+			return result;
+		}
+	
+
+		//IO Tasks
+
+		private void InitFileWatcher () {
+			fileWatcher = new FileSystemWatcher (GetFilePath (), GetFileName ());
+			fileWatcher.Changed += new FileSystemEventHandler (OnFileChanged);
+			fileWatcher.Created += new FileSystemEventHandler (OnFileChanged);
+			fileWatcher.Deleted += new FileSystemEventHandler (OnFileChanged);
+			fileWatcher.EnableRaisingEvents = true;
+		}
+
+		private void OnFileChanged (object source, FileSystemEventArgs e) {
+			Log.Out ("Reloading " + PERMISSIONS_FILE);
+			Load ();
+		}
+
+		private string GetFilePath () {
+			return GamePrefs.GetString (EnumGamePrefs.SaveGameFolder);
+		}
+
+		private string GetFileName () {
+			return PERMISSIONS_FILE;
+		}
+
+		private string GetFullPath () {
+			return GetFilePath () + "/" + GetFileName ();
+		}
+
+		public void Load () {
+			admintokens = new Dictionary<string, AdminToken> ();
+			modules = new Dictionary<string, WebModulePermission> ();
+
+			if (!Utils.FileExists (GetFullPath ())) {
+				Log.Out (string.Format ("Permissions file '{0}' not found, creating.", GetFileName ()));
+				Save ();
+				return;
+			}
+
+			Log.Out (string.Format ("Loading permissions file at '{0}'", GetFullPath ()));
+
+			XmlDocument xmlDoc = new XmlDocument ();
+
+			try {
+				xmlDoc.Load (GetFullPath ());
+			} catch (XmlException e) {
+				Log.Error (string.Format ("Failed loading permissions file: {0}", e.Message));
+				return;
+			}
+
+			XmlNode adminToolsNode = xmlDoc.DocumentElement;
+
+			foreach (XmlNode childNode in adminToolsNode.ChildNodes) {
+				if (childNode.Name == "admintokens") {
+					foreach (XmlNode subChild in childNode.ChildNodes) {
+						if (subChild.NodeType == XmlNodeType.Comment) {
+							continue;
+						}
+						if (subChild.NodeType != XmlNodeType.Element) {
+							Log.Warning ("Unexpected XML node found in 'admintokens' section: " + subChild.OuterXml);
+							continue;
+						}
+
+						XmlElement lineItem = (XmlElement)subChild;
+
+						if (!lineItem.HasAttribute ("name")) {
+							Log.Warning ("Ignoring admintoken-entry because of missing 'name' attribute: " + subChild.OuterXml);
+							continue;
+						}
+
+						if (!lineItem.HasAttribute ("token")) {
+							Log.Warning ("Ignoring admintoken-entry because of missing 'token' attribute: " + subChild.OuterXml);
+							continue;
+						}
+
+						if (!lineItem.HasAttribute ("permission_level")) {
+							Log.Warning ("Ignoring admintoken-entry because of missing 'permission_level' attribute: " + subChild.OuterXml);
+							continue;
+						}
+
+						string name = lineItem.GetAttribute ("name");
+						string token = lineItem.GetAttribute ("token");
+						int permissionLevel = 2000;
+						if (!int.TryParse (lineItem.GetAttribute ("permission_level"), out permissionLevel)) {
+							Log.Warning ("Ignoring admintoken-entry because of invalid (non-numeric) value for 'permission_level' attribute: " + subChild.OuterXml);
+							continue;
+						}
+
+						AddAdmin (name, token, permissionLevel, false); 
+					
+					}
+				}
+
+				if (childNode.Name == "permissions") {
+					foreach (XmlNode subChild in childNode.ChildNodes) {
+						if (subChild.NodeType == XmlNodeType.Comment) {
+							continue;
+						}
+						if (subChild.NodeType != XmlNodeType.Element) {
+							Log.Warning ("Unexpected XML node found in 'permissions' section: " + subChild.OuterXml);
+							continue;
+						}
+
+						XmlElement lineItem = (XmlElement)subChild;
+
+						if (!lineItem.HasAttribute ("module")) {
+							Log.Warning ("Ignoring permission-entry because of missing 'module' attribute: " + subChild.OuterXml);
+							continue;
+						}
+					
+						if (!lineItem.HasAttribute ("permission_level")) {
+							Log.Warning ("Ignoring permission-entry because of missing 'permission_level' attribute: " + subChild.OuterXml);
+							continue;
+						}
+					
+						int permissionLevel = 0;
+						if (!int.TryParse (lineItem.GetAttribute ("permission_level"), out permissionLevel)) {
+							Log.Warning ("Ignoring permission-entry because of invalid (non-numeric) value for 'permission_level' attribute: " + subChild.OuterXml);
+							continue;
+						}
+
+						AddModulePermission (lineItem.GetAttribute ("module").ToLower (), permissionLevel, false); 
+					}
+				}
+
+			}
+
+			Log.Out ("Loading permissions file done.");
+		}
+
+		public void Save () {
+			fileWatcher.EnableRaisingEvents = false;
+
+			using (StreamWriter sw = new StreamWriter(GetFullPath ())) {
+				sw.WriteLine ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
+				sw.WriteLine ("<webpermissions>");
+				sw.WriteLine ("    <admintokens>");
+				sw.WriteLine ("        <!-- <token name=\"adminuser1\" token=\"supersecrettoken\" permission_level=\"0\" /> -->");
+				foreach (AdminToken at in admintokens.Values) {
+					sw.WriteLine (string.Format ("        <token name=\"{0}\" token=\"{1}\" permission_level=\"{2}\" />", at.name, at.token, at.permissionLevel));
+				}
+				sw.WriteLine ("    </admintokens>");
+				sw.WriteLine ();
+				sw.WriteLine ("    <permissions>");
+				sw.WriteLine ("        <!-- <permission module=\"webapi.executeconsolecommand\" permission_level=\"0\" /> -->");
+				sw.WriteLine ("        <!-- <permission module=\"webapi.getplayersonline\" permission_level=\"1\" /> -->");
+				sw.WriteLine ("        <!-- <permission module=\"web.map\" permission_level=\"1000\" /> -->");
+				foreach (WebModulePermission wap in modules.Values) {
+					sw.WriteLine (string.Format ("        <permission module=\"{0}\" permission_level=\"{1}\" />", wap.module, wap.permissionLevel));
+				}
+				sw.WriteLine ("    </permissions>");
+				sw.WriteLine ();
+				sw.WriteLine ("</webpermissions>");
+			
+				sw.Flush ();
+				sw.Close ();
+			}
+
+			fileWatcher.EnableRaisingEvents = true;
+		}
+
+
+		
+		public class AdminToken {
+			public string name;
+			public string token;
+			public int permissionLevel;
+
+			public AdminToken (string _name, string _token, int _permissionLevel) {
+				name = _name;
+				token = _token;
+				permissionLevel = _permissionLevel;
+			}
+		}
+
+		public struct WebModulePermission {
+			public string module;
+			public int permissionLevel;
+
+			public WebModulePermission (string _module, int _permissionLevel) {
+				module = _module;
+				permissionLevel = _permissionLevel;
+			}
+		}
+
+
+	}
+}
+
