Index: /binary-improvements2/MapRendering/API.cs
===================================================================
--- /binary-improvements2/MapRendering/API.cs	(revision 381)
+++ /binary-improvements2/MapRendering/API.cs	(revision 382)
@@ -5,4 +5,5 @@
 	public class API : IModApi {
 		private Web webInstance;
+		private Mod modInstance;
 		
 		public void InitMod (Mod _modInstance) {
@@ -10,4 +11,5 @@
 			ModEvents.GameShutdown.RegisterHandler (GameShutdown);
 			ModEvents.CalcChunkColorsDone.RegisterHandler (CalcChunkColorsDone);
+			modInstance = _modInstance;
 		}
 
@@ -18,5 +20,5 @@
 			}
 			
-			webInstance = new Web ();
+			webInstance = new Web (modInstance.Path);
 			LogBuffer.Init ();
 
Index: /binary-improvements2/MapRendering/Commands/Exception.cs
===================================================================
--- /binary-improvements2/MapRendering/Commands/Exception.cs	(revision 382)
+++ /binary-improvements2/MapRendering/Commands/Exception.cs	(revision 382)
@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+
+namespace AllocsFixes.CustomCommands {
+	public class ConsoleCmdException : ConsoleCmdAbstract {
+		public override string[] GetCommands () {
+			return new[] { "exception" };
+		}
+
+		public override bool AllowedInMainMenu => true;
+
+		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
+			Log.Out ("Test info");
+			Log.Warning ("Test warning");
+			Log.Error ("Test error");
+			throw new Exception ("Test exception");
+		}
+
+		public override string GetDescription () {
+			return "Throw an exception / log messages";
+		}
+	}
+}
Index: /binary-improvements2/MapRendering/Web/API/ExecuteConsoleCommand.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/API/ExecuteConsoleCommand.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/API/ExecuteConsoleCommand.cs	(revision 382)
@@ -1,4 +1,6 @@
 using System;
 using System.Net;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web.API {
Index: /binary-improvements2/MapRendering/Web/API/GetAllowedCommands.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/API/GetAllowedCommands.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/API/GetAllowedCommands.cs	(revision 382)
@@ -1,4 +1,6 @@
 using System.Net;
 using AllocsFixes.JSON;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web.API {
Index: /binary-improvements2/MapRendering/Web/API/GetAnimalsLocation.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/API/GetAnimalsLocation.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/API/GetAnimalsLocation.cs	(revision 382)
@@ -3,4 +3,6 @@
 using AllocsFixes.JSON;
 using AllocsFixes.LiveData;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web.API {
Index: /binary-improvements2/MapRendering/Web/API/GetHostileLocation.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/API/GetHostileLocation.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/API/GetHostileLocation.cs	(revision 382)
@@ -3,4 +3,6 @@
 using AllocsFixes.JSON;
 using AllocsFixes.LiveData;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web.API {
Index: /binary-improvements2/MapRendering/Web/API/GetLandClaims.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/API/GetLandClaims.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/API/GetLandClaims.cs	(revision 382)
@@ -3,4 +3,6 @@
 using AllocsFixes.JSON;
 using AllocsFixes.PersistentData;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web.API {
Index: /binary-improvements2/MapRendering/Web/API/GetLog.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/API/GetLog.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/API/GetLog.cs	(revision 382)
@@ -2,4 +2,6 @@
 using System.Net;
 using AllocsFixes.JSON;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web.API {
@@ -9,7 +11,5 @@
 		public override void HandleRequest (HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _user,
 			int _permissionLevel) {
-			int count, firstLine, lastLine;
-
-			if (_req.QueryString ["count"] == null || !int.TryParse (_req.QueryString ["count"], out count)) {
+			if (_req.QueryString ["count"] == null || !int.TryParse (_req.QueryString ["count"], out int count)) {
 				count = 50;
 			}
@@ -27,15 +27,11 @@
 			}
 
-			if (_req.QueryString ["firstLine"] == null || !int.TryParse (_req.QueryString ["firstLine"], out firstLine)) {
-				if (count > 0) {
-					firstLine = LogBuffer.Instance.OldestLine;
-				} else {
-					firstLine = LogBuffer.Instance.LatestLine;
-				}
+			if (_req.QueryString ["firstLine"] == null || !int.TryParse (_req.QueryString ["firstLine"], out int firstLine)) {
+				firstLine = count > 0 ? LogBuffer.Instance.OldestLine : LogBuffer.Instance.LatestLine;
 			}
 
 			JSONObject result = new JSONObject ();
 
-			List<LogBuffer.LogEntry> logEntries = LogBuffer.Instance.GetRange (ref firstLine, count, out lastLine);
+			List<LogBuffer.LogEntry> logEntries = LogBuffer.Instance.GetRange (ref firstLine, count, out int lastLine);
 
 			JSONArray entries = new JSONArray ();
@@ -44,5 +40,6 @@
 				entry.Add ("date", new JSONString (logEntry.date));
 				entry.Add ("time", new JSONString (logEntry.time));
-				entry.Add ("uptime", new JSONString (logEntry.uptime));
+				entry.Add ("isotime", new JSONString (logEntry.isoTime));
+				entry.Add ("uptime", new JSONString (logEntry.uptime.ToString ()));
 				entry.Add ("msg", new JSONString (logEntry.message));
 				entry.Add ("trace", new JSONString (logEntry.trace));
Index: /binary-improvements2/MapRendering/Web/API/GetPlayerInventories.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/API/GetPlayerInventories.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/API/GetPlayerInventories.cs	(revision 382)
@@ -3,4 +3,6 @@
 using AllocsFixes.JSON;
 using AllocsFixes.PersistentData;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web.API {
Index: /binary-improvements2/MapRendering/Web/API/GetPlayerInventory.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/API/GetPlayerInventory.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/API/GetPlayerInventory.cs	(revision 382)
@@ -3,9 +3,11 @@
 using AllocsFixes.JSON;
 using AllocsFixes.PersistentData;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web.API {
 	public class GetPlayerInventory : WebAPI {
-		public override void HandleRequest (HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _user,
-			int _permissionLevel) {
+		public override void HandleRequest (HttpListenerRequest _req, HttpListenerResponse _resp,
+			WebConnection _user, int _permissionLevel) {
 			if (_req.QueryString ["userid"] == null) {
 				_resp.StatusCode = (int) HttpStatusCode.BadRequest;
Index: /binary-improvements2/MapRendering/Web/API/GetPlayerList.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/API/GetPlayerList.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/API/GetPlayerList.cs	(revision 382)
@@ -6,4 +6,6 @@
 using AllocsFixes.JSON;
 using AllocsFixes.PersistentData;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web.API {
Index: /binary-improvements2/MapRendering/Web/API/GetPlayersLocation.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/API/GetPlayersLocation.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/API/GetPlayersLocation.cs	(revision 382)
@@ -3,4 +3,6 @@
 using AllocsFixes.JSON;
 using AllocsFixes.PersistentData;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web.API {
Index: /binary-improvements2/MapRendering/Web/API/GetPlayersOnline.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/API/GetPlayersOnline.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/API/GetPlayersOnline.cs	(revision 382)
@@ -3,4 +3,6 @@
 using AllocsFixes.JSON;
 using AllocsFixes.PersistentData;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web.API {
Index: /binary-improvements2/MapRendering/Web/API/GetServerInfo.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/API/GetServerInfo.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/API/GetServerInfo.cs	(revision 382)
@@ -2,4 +2,6 @@
 using System.Net;
 using AllocsFixes.JSON;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web.API {
Index: /binary-improvements2/MapRendering/Web/API/GetStats.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/API/GetStats.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/API/GetStats.cs	(revision 382)
@@ -2,4 +2,6 @@
 using AllocsFixes.JSON;
 using AllocsFixes.LiveData;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web.API {
Index: /binary-improvements2/MapRendering/Web/API/GetWebUIUpdates.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/API/GetWebUIUpdates.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/API/GetWebUIUpdates.cs	(revision 382)
@@ -2,4 +2,6 @@
 using AllocsFixes.JSON;
 using AllocsFixes.LiveData;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web.API {
Index: /binary-improvements2/MapRendering/Web/API/Null.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/API/Null.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/API/Null.cs	(revision 382)
@@ -1,4 +1,6 @@
 ﻿using System.Net;
 using System.Text;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web.API {
Index: /binary-improvements2/MapRendering/Web/API/WebAPI.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/API/WebAPI.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/API/WebAPI.cs	(revision 382)
@@ -1,5 +1,6 @@
-using System.Net;
 using System.Text;
 using AllocsFixes.JSON;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web.API {
@@ -44,6 +45,6 @@
 		}
 
-		public abstract void HandleRequest (HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _user,
-			int _permissionLevel);
+		public abstract void HandleRequest (HttpListenerRequest _req, HttpListenerResponse _resp,
+			WebConnection _user, int _permissionLevel);
 
 		public virtual int DefaultPermissionLevel () {
Index: /binary-improvements2/MapRendering/Web/ConnectionHandler.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/ConnectionHandler.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/ConnectionHandler.cs	(revision 382)
@@ -9,9 +9,7 @@
 
 		public WebConnection IsLoggedIn (string _sessionId, IPAddress _ip) {
-			if (!connections.ContainsKey (_sessionId)) {
+			if (!connections.TryGetValue (_sessionId, out WebConnection con)) {
 				return null;
 			}
-
-			WebConnection con = connections [_sessionId];
 
 //			if (con.Age.TotalMinutes > parent.sessionTimeoutMinutes) {
Index: /binary-improvements2/MapRendering/Web/Handlers/AbsHandler.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/Handlers/AbsHandler.cs	(revision 382)
+++ /binary-improvements2/MapRendering/Web/Handlers/AbsHandler.cs	(revision 382)
@@ -0,0 +1,34 @@
+using System.Net;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
+
+namespace AllocsFixes.NetConnections.Servers.Web.Handlers {
+	public abstract class AbsHandler {
+		protected readonly string moduleName;
+		protected string urlBasePath;
+		protected Web parent;
+
+		public string ModuleName => moduleName;
+		public string UrlBasePath => urlBasePath;
+
+		protected AbsHandler (string _moduleName, int _defaultPermissionLevel = 0) {
+			moduleName = _moduleName;
+			WebPermissions.Instance.AddKnownModule (_moduleName, _defaultPermissionLevel);
+		}
+
+		public abstract void HandleRequest (string _requestPath, HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _con,
+			int _permissionLevel);
+
+		public virtual bool IsAuthorizedForHandler (WebConnection _user, int _permissionLevel) {
+			return moduleName == null || WebPermissions.Instance.ModuleAllowedWithLevel (moduleName, _permissionLevel);
+		}
+
+		public virtual void Shutdown () {
+		}
+
+		public virtual void SetBasePathAndParent (Web _parent, string _relativePath) {
+			parent = _parent;
+			urlBasePath = _relativePath;
+		}
+	}
+}
Index: /binary-improvements2/MapRendering/Web/Handlers/ApiHandler.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/Handlers/ApiHandler.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/Handlers/ApiHandler.cs	(revision 382)
@@ -4,7 +4,9 @@
 using System.Reflection;
 using AllocsFixes.NetConnections.Servers.Web.API;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web.Handlers {
-	public class ApiHandler : PathHandler {
+	public class ApiHandler : AbsHandler {
 		private readonly Dictionary<string, WebAPI> apis = new CaseInsensitiveStringDictionary<WebAPI> ();
 
@@ -35,7 +37,7 @@
 #endif
 
-		public override void HandleRequest (HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _user,
+		public override void HandleRequest (string _requestPath, HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _con,
 			int _permissionLevel) {
-			string apiName = _req.Url.AbsolutePath.Remove (0, urlBasePath.Length);
+			string apiName = _requestPath.Remove (0, urlBasePath.Length);
 
 			if (!apis.TryGetValue (apiName, out WebAPI api)) {
@@ -47,5 +49,5 @@
 			if (!AuthorizeForApi (apiName, _permissionLevel)) {
 				_resp.StatusCode = (int) HttpStatusCode.Forbidden;
-				if (_user != null) {
+				if (_con != null) {
 					//Log.Out ($"{nameof(ApiHandler)}: user '{user.SteamID}' not allowed to execute '{apiName}'");
 				}
@@ -58,5 +60,5 @@
 				apiHandlerSampler.Begin ();
 #endif
-				api.HandleRequest (_req, _resp, _user, _permissionLevel);
+				api.HandleRequest (_req, _resp, _con, _permissionLevel);
 #if ENABLE_PROFILER
 				apiHandlerSampler.End ();
Index: /binary-improvements2/MapRendering/Web/Handlers/ItemIconHandler.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/Handlers/ItemIconHandler.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/Handlers/ItemIconHandler.cs	(revision 382)
@@ -4,8 +4,10 @@
 using System.Net;
 using UnityEngine;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 using Object = UnityEngine.Object;
 
 namespace AllocsFixes.NetConnections.Servers.Web.Handlers {
-	public class ItemIconHandler : PathHandler {
+	public class ItemIconHandler : AbsHandler {
 		private readonly Dictionary<string, byte[]> icons = new Dictionary<string, byte[]> ();
 		private readonly bool logMissingFiles;
@@ -24,5 +26,5 @@
 		public static ItemIconHandler Instance { get; private set; }
 
-		public override void HandleRequest (HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _user,
+		public override void HandleRequest (string _requestPath, HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _con,
 			int _permissionLevel) {
 			if (!loaded) {
@@ -32,8 +34,8 @@
 			}
 
-			string requestFileName = _req.Url.AbsolutePath.Remove (0, urlBasePath.Length);
+			string requestFileName = _requestPath.Remove (0, urlBasePath.Length);
 			requestFileName = requestFileName.Remove (requestFileName.LastIndexOf ('.'));
 
-			if (icons.ContainsKey (requestFileName) && _req.Url.AbsolutePath.EndsWith (".png", StringComparison.OrdinalIgnoreCase)) {
+			if (icons.ContainsKey (requestFileName) && _requestPath.EndsWith (".png", StringComparison.OrdinalIgnoreCase)) {
 				_resp.ContentType = MimeType.GetMimeType (".png");
 
@@ -45,5 +47,5 @@
 				_resp.StatusCode = (int) HttpStatusCode.NotFound;
 				if (logMissingFiles) {
-					Log.Out ("Web:IconHandler:FileNotFound: \"" + _req.Url.AbsolutePath + "\" ");
+					Log.Out ("Web:IconHandler:FileNotFound: \"" + _requestPath + "\" ");
 				}
 			}
Index: nary-improvements2/MapRendering/Web/Handlers/PathHandler.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/Handlers/PathHandler.cs	(revision 381)
+++ 	(revision )
@@ -1,31 +1,0 @@
-using System.Net;
-
-namespace AllocsFixes.NetConnections.Servers.Web.Handlers {
-	public abstract class PathHandler {
-		protected readonly string moduleName;
-		protected string urlBasePath;
-		protected Web parent;
-
-		protected PathHandler (string _moduleName, int _defaultPermissionLevel = 0) {
-			moduleName = _moduleName;
-			WebPermissions.Instance.AddKnownModule (_moduleName, _defaultPermissionLevel);
-		}
-
-		public string ModuleName => moduleName;
-
-		public abstract void HandleRequest (HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _user,
-			int _permissionLevel);
-
-		public virtual bool IsAuthorizedForHandler (WebConnection _user, int _permissionLevel) {
-			return moduleName == null || WebPermissions.Instance.ModuleAllowedWithLevel (moduleName, _permissionLevel);
-		}
-
-		public virtual void Shutdown () {
-		}
-
-		public virtual void SetBasePathAndParent (Web _parent, string _relativePath) {
-			parent = _parent;
-			urlBasePath = _relativePath;
-		}
-	}
-}
Index: /binary-improvements2/MapRendering/Web/Handlers/RewriteHandler.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/Handlers/RewriteHandler.cs	(revision 382)
+++ /binary-improvements2/MapRendering/Web/Handlers/RewriteHandler.cs	(revision 382)
@@ -0,0 +1,20 @@
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
+
+namespace AllocsFixes.NetConnections.Servers.Web.Handlers {
+	public class RewriteHandler : AbsHandler {
+		private readonly string target;
+		private readonly bool fixedTarget;
+
+		public RewriteHandler (string _target, bool _fixedTarget = false) : base (null) {
+			target = _target;
+			fixedTarget = _fixedTarget;
+		}
+
+		public override void HandleRequest (string _requestPath, HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _con,
+			int _permissionLevel) {
+			string newRequestPath = fixedTarget ? target : target + _requestPath.Remove (0, urlBasePath.Length);
+			parent.ApplyPathHandler (newRequestPath, _req, _resp, _con, _permissionLevel);
+		}
+	}
+}
Index: /binary-improvements2/MapRendering/Web/Handlers/SessionHandler.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/Handlers/SessionHandler.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/Handlers/SessionHandler.cs	(revision 382)
@@ -1,12 +1,23 @@
+using System;
 using System.IO;
 using System.Net;
 using System.Text;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web.Handlers {
-	public class SessionHandler : PathHandler {
+	public class SessionHandler : AbsHandler {
+		private const string pageBasePath = "/";
+		private const string steamOpenIdVerifyUrl = "verifysteamopenid";
+		private const string steamLoginUrl = "loginsteam";
+		
 		private readonly string footer = "";
 		private readonly string header = "";
 
-		public SessionHandler (string _dataFolder, string _moduleName = null) : base (_moduleName) {
+		private readonly ConnectionHandler connectionHandler;
+
+		public SessionHandler (string _dataFolder, ConnectionHandler _connectionHandler) : base (null) {
+			connectionHandler = _connectionHandler;
+			
 			if (File.Exists (_dataFolder + "/sessionheader.tmpl")) {
 				header = File.ReadAllText (_dataFolder + "/sessionheader.tmpl");
@@ -18,39 +29,66 @@
 		}
 
-		public override void HandleRequest (HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _user,
+		public override void HandleRequest (string _requestPath, HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _con,
 			int _permissionLevel) {
-			string subpath = _req.Url.AbsolutePath.Remove (0, urlBasePath.Length);
+			
+			IPEndPoint reqRemoteEndPoint = _req.RemoteEndPoint;
+			if (reqRemoteEndPoint == null) {
+				_resp.Redirect (pageBasePath);
+				return;
+			}
+
+			string subpath = _requestPath.Remove (0, urlBasePath.Length);
 
 			StringBuilder result = new StringBuilder ();
 			result.Append (header);
 
-			if (subpath.StartsWith ("verify")) {
-				if (_user != null) {
-					_resp.Redirect ("/static/index.html");
+			if (subpath.StartsWith (steamOpenIdVerifyUrl)) {
+				string remoteEndpointString = reqRemoteEndPoint.ToString ();
+
+				try {
+					ulong id = OpenID.Validate (_req);
+					if (id > 0) {
+						WebConnection con = connectionHandler.LogIn (id, reqRemoteEndPoint.Address);
+						int level = GameManager.Instance.adminTools.GetUserPermissionLevel (con.UserId);
+						Log.Out ("Steam OpenID login from {0} with ID {1}, permission level {2}",
+							remoteEndpointString, con.UserId, level);
+						
+						Cookie cookie = new Cookie ("sid", con.SessionID, "/") {
+							Expired = false,
+							Expires = DateTime.MinValue,
+							HttpOnly = true,
+							Secure = false
+						};
+						_resp.AppendCookie (cookie);
+						_resp.Redirect (pageBasePath);
+
+						return;
+					}
+				} catch (Exception e) {
+					Log.Error ("Error validating login:");
+					Log.Exception (e);
+				}
+
+				Log.Out ($"Steam OpenID login failed from {remoteEndpointString}");
+				result.Append ($"<h1>Login failed, <a href=\"{pageBasePath}\">click to return to main page</a>.</h1>");
+			} else if (subpath.StartsWith ("logout")) {
+				if (_con != null) {
+					connectionHandler.LogOut (_con.SessionID);
+					Cookie cookie = new Cookie ("sid", "", "/") {
+						Expired = true
+					};
+					_resp.AppendCookie (cookie);
+					_resp.Redirect (pageBasePath);
 					return;
 				}
 
-				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;
-				}
-
-				result.Append (
-					"<h1>Not logged in, <a href=\"/static/index.html\">click to return to main page</a>.</h1>");
-			} else if (subpath.StartsWith ("login")) {
+				result.Append ($"<h1>Not logged in, <a href=\"{pageBasePath}\">click to return to main page</a>.</h1>");
+			} else if (subpath.StartsWith (steamLoginUrl)) {
 				string host = (Web.IsSslRedirected (_req) ? "https://" : "http://") + _req.UserHostName;
-				string url = OpenID.GetOpenIdLoginUrl (host, host + "/session/verify");
+				string url = OpenID.GetOpenIdLoginUrl (host, host + urlBasePath + steamOpenIdVerifyUrl);
 				_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 ($"<h1>Unknown command, <a href=\"{pageBasePath}\">click to return to main page</a>.</h1>");
 			}
 
Index: /binary-improvements2/MapRendering/Web/Handlers/SimpleRedirectHandler.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/Handlers/SimpleRedirectHandler.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/Handlers/SimpleRedirectHandler.cs	(revision 382)
@@ -1,13 +1,15 @@
 using System.Net;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web.Handlers {
-	public class SimpleRedirectHandler : PathHandler {
+	public class SimpleRedirectHandler : AbsHandler {
 		private readonly string target;
 
-		public SimpleRedirectHandler (string _target, string _moduleName = null) : base (_moduleName) {
+		public SimpleRedirectHandler (string _target) : base (null) {
 			target = _target;
 		}
 
-		public override void HandleRequest (HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _user,
+		public override void HandleRequest (string _requestPath, HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _con,
 			int _permissionLevel) {
 			_resp.Redirect (target);
Index: /binary-improvements2/MapRendering/Web/Handlers/StaticHandler.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/Handlers/StaticHandler.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/Handlers/StaticHandler.cs	(revision 382)
@@ -2,7 +2,9 @@
 using System.Net;
 using AllocsFixes.FileCache;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web.Handlers {
-	public class StaticHandler : PathHandler {
+	public class StaticHandler : AbsHandler {
 		private readonly AbstractCache cache;
 		private readonly string datapath;
@@ -16,7 +18,7 @@
 		}
 
-		public override void HandleRequest (HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _user,
+		public override void HandleRequest (string _requestPath, HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _con,
 			int _permissionLevel) {
-			string fn = _req.Url.AbsolutePath.Remove (0, urlBasePath.Length);
+			string fn = _requestPath.Remove (0, urlBasePath.Length);
 
 			byte[] content = cache.GetFileContent (datapath + fn);
@@ -29,5 +31,5 @@
 				_resp.StatusCode = (int) HttpStatusCode.NotFound;
 				if (logMissingFiles) {
-					Log.Out ("Web:Static:FileNotFound: \"" + _req.Url.AbsolutePath + "\" @ \"" + datapath + fn + "\"");
+					Log.Out ("Web:Static:FileNotFound: \"" + _requestPath + "\" @ \"" + datapath + fn + "\"");
 				}
 			}
Index: /binary-improvements2/MapRendering/Web/Handlers/UserStatusHandler.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/Handlers/UserStatusHandler.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/Handlers/UserStatusHandler.cs	(revision 382)
@@ -1,17 +1,18 @@
-using System.Net;
 using AllocsFixes.JSON;
 using AllocsFixes.NetConnections.Servers.Web.API;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web.Handlers {
-	public class UserStatusHandler : PathHandler {
+	public class UserStatusHandler : AbsHandler {
 		public UserStatusHandler (string _moduleName = null) : base (_moduleName) {
 		}
 
-		public override void HandleRequest (HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _user,
+		public override void HandleRequest (string _requestPath, HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _con,
 			int _permissionLevel) {
 			JSONObject result = new JSONObject ();
 
-			result.Add ("loggedin", new JSONBoolean (_user != null));
-			result.Add ("username", new JSONString (_user != null ? _user.UserId.ToString () : string.Empty));
+			result.Add ("loggedin", new JSONBoolean (_con != null));
+			result.Add ("username", new JSONString (_con != null ? _con.UserId.ToString () : string.Empty));
 
 			JSONArray perms = new JSONArray ();
Index: /binary-improvements2/MapRendering/Web/LogBuffer.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/LogBuffer.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/LogBuffer.cs	(revision 382)
@@ -5,5 +5,6 @@
 namespace AllocsFixes.NetConnections.Servers.Web {
 	public class LogBuffer {
-		private const int MAX_ENTRIES = 3000;
+		private const int maxEntries = 3000;
+		
 		private static LogBuffer instance;
 
@@ -22,13 +23,5 @@
 		}
 
-		public static LogBuffer Instance {
-			get {
-				if (instance == null) {
-					instance = new LogBuffer ();
-				}
-
-				return instance;
-			}
-		}
+		public static LogBuffer Instance => instance ?? (instance = new LogBuffer ());
 
 		public int OldestLine {
@@ -69,18 +62,11 @@
 
 		private void LogCallback (string _formattedMsg, string _plainMsg, string _trace, LogType _type, DateTime _timestamp, long _uptime) {
-			LogEntry le = new LogEntry ();
-
-			le.date = $"{_timestamp.Year:0000}-{_timestamp.Month:00}-{_timestamp.Day:00}";
-			le.time = $"{_timestamp.Hour:00}:{_timestamp.Minute:00}:{_timestamp.Second:00}";
-			le.uptime = _uptime.ToString ();
-			le.message = _plainMsg;
-			le.trace = _trace;
-			le.type = _type;
+			LogEntry le = new LogEntry (_timestamp, _plainMsg, _trace, _type, _uptime);
 
 			lock (logEntries) {
 				logEntries.Add (le);
-				if (logEntries.Count > MAX_ENTRIES) {
-					listOffset += logEntries.Count - MAX_ENTRIES;
-					logEntries.RemoveRange (0, logEntries.Count - MAX_ENTRIES);
+				if (logEntries.Count > maxEntries) {
+					listOffset += logEntries.Count - maxEntries;
+					logEntries.RemoveRange (0, logEntries.Count - maxEntries);
 				}
 			}
@@ -140,10 +126,24 @@
 
 		public class LogEntry {
-			public string date;
-			public string message;
-			public string time;
-			public string trace;
-			public LogType type;
-			public string uptime;
+			public readonly DateTime timestamp;
+			public readonly string date;
+			public readonly string time;
+			public readonly string isoTime;
+			public readonly string message;
+			public readonly string trace;
+			public readonly LogType type;
+			public readonly long uptime;
+
+			public LogEntry (DateTime _timestamp, string _message, string _trace, LogType _type, long _uptime) {
+				timestamp = _timestamp;
+				date = $"{_timestamp.Year:0000}-{_timestamp.Month:00}-{_timestamp.Day:00}";
+				time = $"{_timestamp.Hour:00}:{_timestamp.Minute:00}:{_timestamp.Second:00}";
+				isoTime = _timestamp.ToString ("o");
+
+				message = _message;
+				trace = _trace;
+				type = _type;
+				uptime = _uptime;
+			}
 		}
 	}
Index: /binary-improvements2/MapRendering/Web/OpenID.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/OpenID.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/OpenID.cs	(revision 382)
@@ -9,4 +9,5 @@
 using System.Text;
 using System.Text.RegularExpressions;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
 
 namespace AllocsFixes.NetConnections.Servers.Web {
Index: /binary-improvements2/MapRendering/Web/SSE/EventBase.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/SSE/EventBase.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/SSE/EventBase.cs	(revision 382)
@@ -2,8 +2,8 @@
 using System.Collections.Generic;
 using System.IO;
-using System.Net;
 using System.Net.Sockets;
 using System.Text;
 using AllocsFixes.JSON;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web.SSE {
@@ -116,6 +116,4 @@
 							Log.Exception (e);
 						}
-
-						resp.Close ();
 					} catch (Exception e) {
 						currentlyOpen--;
Index: /binary-improvements2/MapRendering/Web/SSE/EventLog.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/SSE/EventLog.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/SSE/EventLog.cs	(revision 382)
@@ -12,4 +12,5 @@
 			string date = $"{_timestamp.Year:0000}-{_timestamp.Month:00}-{_timestamp.Day:00}";
 			string time = $"{_timestamp.Hour:00}:{_timestamp.Minute:00}:{_timestamp.Second:00}";
+			string isotime = _timestamp.ToString ("o");
 			string uptime = _uptime.ToString ();
 			string message = _plainMsg;
@@ -21,4 +22,5 @@
 			data.Add ("date", new JSONString (date));
 			data.Add ("time", new JSONString (time));
+			data.Add ("isotime", new JSONString (isotime));
 			data.Add ("uptime", new JSONString (uptime));
 
Index: /binary-improvements2/MapRendering/Web/SSE/SseHandler.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/SSE/SseHandler.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/SSE/SseHandler.cs	(revision 382)
@@ -5,4 +5,6 @@
 using System.Threading;
 using AllocsFixes.NetConnections.Servers.Web.Handlers;
+using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 // Implemented following HTML spec
@@ -10,7 +12,7 @@
 
 namespace AllocsFixes.NetConnections.Servers.Web.SSE {
-	public class SseHandler : PathHandler {
+	public class SseHandler : AbsHandler {
 		private readonly Dictionary<string, EventBase> events = new CaseInsensitiveStringDictionary<EventBase> ();
-		
+
 		private ThreadManager.ThreadInfo queueThead;
 		private readonly AutoResetEvent evSendRequest = new AutoResetEvent (false);
@@ -18,6 +20,6 @@
 
 		public SseHandler (string _moduleName = null) : base (_moduleName) {
-			Type[] ctorTypes = {typeof (SseHandler)};
-			object[] ctorParams = {this};
+			Type[] ctorTypes = { typeof (SseHandler) };
+			object[] ctorParams = { this };
 
 			foreach (Type t in Assembly.GetExecutingAssembly ().GetTypes ()) {
@@ -25,6 +27,6 @@
 					ConstructorInfo ctor = t.GetConstructor (ctorTypes);
 					if (ctor != null) {
-						EventBase apiInstance = (EventBase) ctor.Invoke (ctorParams);
-						addEvent (apiInstance.Name, apiInstance);
+						EventBase apiInstance = (EventBase)ctor.Invoke (ctorParams);
+						AddEvent (apiInstance.Name, apiInstance);
 					}
 				}
@@ -34,5 +36,5 @@
 		public override void SetBasePathAndParent (Web _parent, string _relativePath) {
 			base.SetBasePathAndParent (_parent, _relativePath);
-			
+
 			queueThead = ThreadManager.StartThread ("SSE-Processing_" + urlBasePath, QueueProcessThread, ThreadPriority.BelowNormal,
 				_useRealThread: true);
@@ -45,22 +47,22 @@
 		}
 
-		private void addEvent (string _eventName, EventBase _eventInstance) {
+		public void AddEvent (string _eventName, EventBase _eventInstance) {
 			events.Add (_eventName, _eventInstance);
 			WebPermissions.Instance.AddKnownModule ("webevent." + _eventName, _eventInstance.DefaultPermissionLevel ());
 		}
 
-		public override void HandleRequest (HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _user,
+		public override void HandleRequest (string _requestPath, HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _con,
 			int _permissionLevel) {
-			string eventName = _req.Url.AbsolutePath.Remove (0, urlBasePath.Length);
+			string eventName = _requestPath.Remove (0, urlBasePath.Length);
 
 			if (!events.TryGetValue (eventName, out EventBase eventInstance)) {
 				Log.Out ($"Error in {nameof (SseHandler)}.HandleRequest(): No handler found for event \"{eventName}\"");
-				_resp.StatusCode = (int) HttpStatusCode.NotFound;
+				_resp.StatusCode = (int)HttpStatusCode.NotFound;
 				return;
 			}
 
-			if (!AuthorizeForEvent (eventName, _permissionLevel)) {
-				_resp.StatusCode = (int) HttpStatusCode.Forbidden;
-				if (_user != null) {
+			if (!IsAuthorizedForEvent (eventName, _permissionLevel)) {
+				_resp.StatusCode = (int)HttpStatusCode.Forbidden;
+				if (_con != null) {
 					//Log.Out ($"{nameof(SseHandler)}: user '{user.SteamID}' not allowed to access '{eventName}'");
 				}
@@ -80,24 +82,24 @@
 				Log.Error ($"Error in {nameof (SseHandler)}.HandleRequest(): Handler {eventInstance.Name} threw an exception:");
 				Log.Exception (e);
-				_resp.StatusCode = (int) HttpStatusCode.InternalServerError;
+				_resp.StatusCode = (int)HttpStatusCode.InternalServerError;
 			}
 		}
 
-		private bool AuthorizeForEvent (string _eventName, int _permissionLevel) {
+		private bool IsAuthorizedForEvent (string _eventName, int _permissionLevel) {
 			return WebPermissions.Instance.ModuleAllowedWithLevel ("webevent." + _eventName, _permissionLevel);
 		}
 
 		private void QueueProcessThread (ThreadManager.ThreadInfo _threadInfo) {
-			try {
-				while (!shutdown && !_threadInfo.TerminationRequested ()) {
-					evSendRequest.WaitOne (500);
+			while (!shutdown && !_threadInfo.TerminationRequested ()) {
+				evSendRequest.WaitOne (500);
 
-					foreach (KeyValuePair<string, EventBase> kvp in events) {
+				foreach (KeyValuePair<string, EventBase> kvp in events) {
+					try {
 						kvp.Value.ProcessSendQueue ();
+					} catch (Exception e) {
+						Log.Error ($"SSE ({kvp.Key}): Error processing send queue");
+						Log.Exception (e);
 					}
 				}
-			} catch (Exception e) {
-				Log.Error ("SSE: Error processing send queue");
-				Log.Exception (e);
 			}
 		}
Index: /binary-improvements2/MapRendering/Web/Web.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/Web.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/Web.cs	(revision 382)
@@ -2,7 +2,8 @@
 using System.Collections.Generic;
 using System.IO;
-using System.Net;
 using System.Net.Sockets;
-using System.Reflection;
+using Cookie = System.Net.Cookie;
+using HttpStatusCode = System.Net.HttpStatusCode;
+using IPEndPoint = System.Net.IPEndPoint;
 using System.Text;
 using System.Threading;
@@ -10,18 +11,22 @@
 using AllocsFixes.NetConnections.Servers.Web.Handlers;
 using AllocsFixes.NetConnections.Servers.Web.SSE;
+using SpaceWizards.HttpListener;
 using UnityEngine;
 
 namespace AllocsFixes.NetConnections.Servers.Web {
 	public class Web : IConsoleServer {
-		private const int GUEST_PERMISSION_LEVEL = 2000;
+		private const int guestPermissionLevel = 2000;
+		private const string indexPagePath = "/app";
+		
 		public static int handlingCount;
 		public static int currentHandlers;
 		public static long totalHandlingTime = 0;
+		private readonly List<AbsHandler> handlers = new List<AbsHandler> ();
+		private readonly ConnectionHandler connectionHandler;
+
 		private readonly HttpListener listener = new HttpListener ();
-		private readonly Dictionary<string, PathHandler> handlers = new CaseInsensitiveStringDictionary<PathHandler> ();
-
-		public readonly ConnectionHandler connectionHandler;
-
-		public Web () {
+		private readonly Version httpProtocolVersion = new Version(1, 1);
+
+		public Web (string _modInstancePath) {
 			try {
 				int webPort = GamePrefs.GetInt (EnumUtils.Parse<EnumGamePrefs> ("ControlPanelPort"));
@@ -31,7 +36,9 @@
 				}
 
-				if (!Directory.Exists (Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location) +
-				                       "/webserver")) {
-					Log.Out ("Webserver not started (folder \"webserver\" not found in WebInterface mod folder)");
+				// TODO: Remove once this becomes the default control panel
+				webPort += 2;
+
+				if (!HttpListener.IsSupported) {
+					Log.Out ("Webserver not started (needs Windows XP SP2, Server 2003 or later or Mono)");
 					return;
 				}
@@ -40,18 +47,25 @@
 				bool useStaticCache = false;
 
-				string dataFolder = Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location) + "/webserver";
-
-				if (!HttpListener.IsSupported) {
-					Log.Out ("Webserver not started (needs Windows XP SP2, Server 2003 or later or Mono)");
-					return;
-				}
-
-				
-				RegisterPathHandler ("/index.htm", new SimpleRedirectHandler ("/static/index.html"));
-				RegisterPathHandler ("/favicon.ico", new SimpleRedirectHandler ("/static/favicon.ico"));
-				RegisterPathHandler ("/session/", new SessionHandler (dataFolder));
+				string webfilesFolder = _modInstancePath + "/webserver";
+				string webfilesFolderLegacy = _modInstancePath + "/weblegacy";
+
+				connectionHandler = new ConnectionHandler ();
+				
+				RegisterPathHandler ("/", new RewriteHandler ("/files/"));
+
+				// React virtual routing
+				RegisterPathHandler ("/app", new RewriteHandler ("/files/index.html", true));
+				
+				// Legacy web page
+				RegisterPathHandler ("/weblegacy", new StaticHandler (
+					webfilesFolderLegacy,
+					useStaticCache ? (AbstractCache)new SimpleCache () : new DirectAccess (),
+					false)
+				);
+				
+				RegisterPathHandler ("/session/", new SessionHandler (webfilesFolder, connectionHandler));
 				RegisterPathHandler ("/userstatus", new UserStatusHandler ());
-				RegisterPathHandler ("/static/", new StaticHandler (
-						dataFolder,
+				RegisterPathHandler ("/files/", new StaticHandler (
+						webfilesFolder,
 						useStaticCache ? (AbstractCache) new SimpleCache () : new DirectAccess (),
 						false)
@@ -67,26 +81,27 @@
 				RegisterPathHandler ("/sse/", new SseHandler ());
 
-				connectionHandler = new ConnectionHandler ();
-
-				listener.Prefixes.Add ($"http://*:{webPort + 2}/");
+				listener.Prefixes.Add ($"http://+:{webPort}/");
+				// listener.Prefixes.Add ($"http://[::1]:{webPort}/");
 				listener.Start ();
+				listener.BeginGetContext (HandleRequest, listener);
 
 				SdtdConsole.Instance.RegisterServer (this);
 
-				listener.BeginGetContext (HandleRequest, listener);
-
-				Log.Out ("Started Webserver on " + (webPort + 2));
+				Log.Out ("Started Webserver on " + webPort);
 			} catch (Exception e) {
-				Log.Out ("Error in Web.ctor: " + e);
-			}
-		}
-
-		public void RegisterPathHandler (string _urlBasePath, PathHandler _handler) {
-			if (handlers.ContainsKey (_urlBasePath)) {
-				Log.Error ($"Web: Handler for relative path {_urlBasePath} already registerd.");
-				return;
+				Log.Error ("Error in Web.ctor: ");
+				Log.Exception (e);
+			}
+		}
+
+		public void RegisterPathHandler (string _urlBasePath, AbsHandler _handler) {
+			foreach (AbsHandler handler in handlers) {
+				if (handler.UrlBasePath == _urlBasePath) {
+					Log.Error ($"Web: Handler for relative path {_urlBasePath} already registerd.");
+					return;
+				}
 			}
 			
-			handlers.Add (_urlBasePath, _handler);
+			handlers.Add (_handler);
 			_handler.SetBasePathAndParent (this, _urlBasePath);
 		}
@@ -102,6 +117,6 @@
 
 		public void Shutdown () {
-			foreach (KeyValuePair<string, PathHandler> kvp in handlers) {
-				kvp.Value.Shutdown ();
+			foreach (AbsHandler handler in handlers) {
+				handler.Shutdown ();
 			}
 		}
@@ -120,6 +135,4 @@
 		}
 		
-		private readonly Version HttpProtocolVersion = new Version(1, 1);
-		
 #if ENABLE_PROFILER
 		private readonly UnityEngine.Profiling.CustomSampler authSampler = UnityEngine.Profiling.CustomSampler.Create ("Auth");
@@ -128,5 +141,6 @@
 
 		private void HandleRequest (IAsyncResult _result) {
-			if (!listener.IsListening) {
+			HttpListener listenerInstance = (HttpListener)_result.AsyncState;
+			if (!listenerInstance.IsListening) {
 				return;
 			}
@@ -135,12 +149,11 @@
 			Interlocked.Increment (ref currentHandlers);
 
-//				MicroStopwatch msw = new MicroStopwatch ();
 #if ENABLE_PROFILER
 			UnityEngine.Profiling.Profiler.BeginThreadProfiling ("AllocsMods", "WebRequest");
-			HttpListenerContext ctx = _listener.EndGetContext (_result);
+			HttpListenerContext ctx = listenerInstance.EndGetContext (_result);
 			try {
 #else
-			HttpListenerContext ctx = listener.EndGetContext (_result);
-			listener.BeginGetContext (HandleRequest, listener);
+			HttpListenerContext ctx = listenerInstance.EndGetContext (_result);
+			listenerInstance.BeginGetContext (HandleRequest, listenerInstance);
 #endif
 			try {
@@ -149,5 +162,5 @@
 				response.SendChunked = false;
 
-				response.ProtocolVersion = HttpProtocolVersion;
+				response.ProtocolVersion = httpProtocolVersion;
 
 #if ENABLE_PROFILER
@@ -179,37 +192,16 @@
 				}
 
-				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 {
-#if ENABLE_PROFILER
-								handlerSampler.Begin ();
-#endif
-								kvp.Value.HandleRequest (request, response, conn, permissionLevel);
-#if ENABLE_PROFILER
-								handlerSampler.End ();
-#endif
-							}
-
-							return;
-						}
-					}
-				}
-
-				// Not really relevant for non-debugging purposes:
-				//Log.Out ("Error in Web.HandleRequest(): No handler found for path \"" + request.Url.AbsolutePath + "\"");
-				response.StatusCode = (int) HttpStatusCode.NotFound;
+				string requestPath = request.Url.AbsolutePath;
+
+				if (requestPath.Length < 2) {
+					response.Redirect (indexPagePath);
+					return;
+				}
+				
+				ApplyPathHandler (requestPath, request, response, conn, permissionLevel);
+
 			} catch (IOException e) {
 				if (e.InnerException is SocketException) {
-					Log.Out ("Error in Web.HandleRequest(): Remote host closed connection: " +
-					         e.InnerException.Message);
+					Log.Out ("Error in Web.HandleRequest(): Remote host closed connection: " + e.InnerException.Message);
 				} else {
 					Log.Out ("Error (IO) in Web.HandleRequest(): " + e);
@@ -219,19 +211,45 @@
 				Log.Exception (e);
 			} finally {
-				if (ctx != null && !ctx.Response.SendChunked) {
+				if (!ctx.Response.SendChunked) {
 					ctx.Response.Close ();
 				}
-
-//					msw.Stop ();
-//					totalHandlingTime += msw.ElapsedMicroseconds;
-//					Log.Out ("Web.HandleRequest(): Took {0} µs", msw.ElapsedMicroseconds);
 				Interlocked.Decrement (ref currentHandlers);
 			}
 #if ENABLE_PROFILER
 			} finally {
-				_listener.BeginGetContext (HandleRequest, _listener);
+				listenerInstance.BeginGetContext (HandleRequest, listenerInstance);
 				UnityEngine.Profiling.Profiler.EndThreadProfiling ();
 			}
 #endif
+		}
+
+		public void ApplyPathHandler (string _requestPath, HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _con,
+			int _permissionLevel) {
+			for (int i = handlers.Count - 1; i >= 0; i--) {
+				AbsHandler handler = handlers [i];
+				
+				if (_requestPath.StartsWith (handler.UrlBasePath)) {
+					if (!handler.IsAuthorizedForHandler (_con, _permissionLevel)) {
+						_resp.StatusCode = (int)HttpStatusCode.Forbidden;
+						if (_con != null) {
+							//Log.Out ("Web.HandleRequest: user '{0}' not allowed to access '{1}'", _con.SteamID, handler.ModuleName);
+						}
+					} else {
+#if ENABLE_PROFILER
+						handlerSampler.Begin ();
+#endif
+						handler.HandleRequest (_requestPath, _req, _resp, _con, _permissionLevel);
+#if ENABLE_PROFILER
+						handlerSampler.End ();
+#endif
+					}
+
+					return;
+				}
+			}
+
+			// Not really relevant for non-debugging purposes:
+			//Log.Out ("Error in Web.HandleRequest(): No handler found for path \"" + _requestPath + "\"");
+			_resp.StatusCode = (int) HttpStatusCode.NotFound;
 		}
 
@@ -239,18 +257,20 @@
 			_con = null;
 
-			string sessionId = null;
-			if (_req.Cookies ["sid"] != null) {
-				sessionId = _req.Cookies ["sid"].Value;
-			}
-
+			string sessionId = _req.Cookies ["sid"]?.Value;
+
+			IPEndPoint reqRemoteEndPoint = _req.RemoteEndPoint;
+			if (reqRemoteEndPoint == null) {
+				Log.Warning ("No RemoteEndPoint on web request");
+				return guestPermissionLevel;
+			}
+			
 			if (!string.IsNullOrEmpty (sessionId)) {
-				WebConnection con = connectionHandler.IsLoggedIn (sessionId, _req.RemoteEndPoint.Address);
-				if (con != null) {
-					_con = con;
+				_con = connectionHandler.IsLoggedIn (sessionId, reqRemoteEndPoint.Address);
+				if (_con != null) {
 					return GameManager.Instance.adminTools.GetUserPermissionLevel (_con.UserId);
 				}
 			}
 
-			string remoteEndpointString = _req.RemoteEndPoint.ToString ();
+			string remoteEndpointString = reqRemoteEndPoint.ToString ();
 
 			if (_req.QueryString ["adminuser"] != null && _req.QueryString ["admintoken"] != null) {
@@ -264,24 +284,5 @@
 			}
 
-			if (_req.Url.AbsolutePath.StartsWith ("/session/verify", StringComparison.OrdinalIgnoreCase)) {
-				try {
-					ulong id = OpenID.Validate (_req);
-					if (id > 0) {
-						WebConnection con = connectionHandler.LogIn (id, _req.RemoteEndPoint.Address);
-						_con = con;
-						int level = GameManager.Instance.adminTools.GetUserPermissionLevel (con.UserId);
-						Log.Out ("Steam OpenID login from {0} with ID {1}, permission level {2}",
-							remoteEndpointString, con.UserId, level);
-						return level;
-					}
-
-					Log.Out ("Steam OpenID login failed from {0}", remoteEndpointString);
-				} catch (Exception e) {
-					Log.Error ("Error validating login:");
-					Log.Exception (e);
-				}
-			}
-
-			return GUEST_PERMISSION_LEVEL;
+			return guestPermissionLevel;
 		}
 
Index: /binary-improvements2/MapRendering/Web/WebCommandResult.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/WebCommandResult.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/WebCommandResult.cs	(revision 382)
@@ -2,5 +2,4 @@
 using System.Collections.Generic;
 using System.IO;
-using System.Net;
 using System.Net.Sockets;
 using System.Text;
@@ -9,4 +8,5 @@
 using AllocsFixes.NetConnections.Servers.Web.API;
 using UnityEngine;
+using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse;
 
 namespace AllocsFixes.NetConnections.Servers.Web {
@@ -27,6 +27,5 @@
 		private readonly ResultType responseType;
 
-		public WebCommandResult (string _command, string _parameters, ResultType _responseType,
-			HttpListenerResponse _response) {
+		public WebCommandResult (string _command, string _parameters, ResultType _responseType, HttpListenerResponse _response) {
 			Interlocked.Increment (ref handlingCount);
 			Interlocked.Increment (ref currentHandlers);
Index: /binary-improvements2/MapRendering/Web/WebConnection.cs
===================================================================
--- /binary-improvements2/MapRendering/Web/WebConnection.cs	(revision 381)
+++ /binary-improvements2/MapRendering/Web/WebConnection.cs	(revision 382)
@@ -11,4 +11,12 @@
 		private readonly string conDescription;
 
+		public string SessionID { get; }
+
+		public IPAddress Endpoint { get; }
+
+		public PlatformUserIdentifierAbs UserId { get; }
+
+		public TimeSpan Age => DateTime.Now - lastAction;
+
 		public WebConnection (string _sessionId, IPAddress _endpoint, PlatformUserIdentifierAbs _userId) {
 			SessionID = _sessionId;
@@ -19,12 +27,4 @@
 			conDescription = "WebPanel from " + Endpoint;
 		}
-
-		public string SessionID { get; }
-
-		public IPAddress Endpoint { get; }
-
-		public PlatformUserIdentifierAbs UserId { get; }
-
-		public TimeSpan Age => DateTime.Now - lastAction;
 
 		public static bool CanViewAllPlayers (int _permissionLevel) {
Index: /binary-improvements2/MapRendering/WebAndMapRendering.csproj
===================================================================
--- /binary-improvements2/MapRendering/WebAndMapRendering.csproj	(revision 381)
+++ /binary-improvements2/MapRendering/WebAndMapRendering.csproj	(revision 382)
@@ -47,4 +47,12 @@
       <Private>False</Private>
     </Reference>
+    <Reference Include="System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
+      <HintPath>..\7dtd-binaries\System.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+    <Reference Include="System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
+      <HintPath>..\7dtd-binaries\System.Xml.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
     <Reference Include="UnityEngine">
       <HintPath>..\7dtd-binaries\UnityEngine.dll</HintPath>
@@ -57,12 +65,4 @@
     <Reference Include="mscorlib">
       <HintPath>..\7dtd-binaries\mscorlib.dll</HintPath>
-      <Private>False</Private>
-    </Reference>
-    <Reference Include="System.Xml">
-      <HintPath>..\7dtd-binaries\System.Xml.dll</HintPath>
-      <Private>False</Private>
-    </Reference>
-    <Reference Include="System">
-      <HintPath>..\7dtd-binaries\System.dll</HintPath>
       <Private>False</Private>
     </Reference>
@@ -78,4 +78,5 @@
   <ItemGroup>
     <Compile Include="AssemblyInfo.cs" />
+    <Compile Include="Commands\Exception.cs" />
     <Compile Include="MapRendering\MapRendering.cs" />
     <Compile Include="MapRendering\MapRenderBlockBuffer.cs" />
@@ -87,4 +88,5 @@
     <Compile Include="Web\API\GetHostileLocation.cs" />
     <Compile Include="Web\API\Null.cs" />
+    <Compile Include="Web\Handlers\RewriteHandler.cs" />
     <Compile Include="Web\SSE\EventLog.cs" />
     <Compile Include="Web\SSE\SseHandler.cs" />
@@ -105,5 +107,5 @@
     <Compile Include="Web\Handlers\ApiHandler.cs" />
     <Compile Include="Web\Handlers\ItemIconHandler.cs" />
-    <Compile Include="Web\Handlers\PathHandler.cs" />
+    <Compile Include="Web\Handlers\AbsHandler.cs" />
     <Compile Include="Web\Handlers\SimpleRedirectHandler.cs" />
     <Compile Include="Web\Handlers\StaticHandler.cs" />
@@ -131,4 +133,8 @@
       <Private>False</Private>
     </ProjectReference>
+    <ProjectReference Include="..\SpaceWizards.HttpListener\SpaceWizards.HttpListener.csproj">
+      <Project>{e273d042-57f9-4e2e-8268-5053527e5287}</Project>
+      <Name>SpaceWizards.HttpListener</Name>
+    </ProjectReference>
   </ItemGroup>
   <ItemGroup>
Index: /binary-improvements2/server-fixes.sln
===================================================================
--- /binary-improvements2/server-fixes.sln	(revision 381)
+++ /binary-improvements2/server-fixes.sln	(revision 382)
@@ -7,4 +7,6 @@
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebAndMapRendering", "MapRendering\WebAndMapRendering.csproj", "{A1847B5F-7BFC-4BCD-94AA-A6C9FB7E7C54}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpaceWizards.HttpListener", "SpaceWizards.HttpListener\SpaceWizards.HttpListener.csproj", "{E273D042-57F9-4E2E-8268-5053527E5287}"
 EndProject
 Global
