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 374)
+++ 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 374)
+++ 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: binary-improvements2/MapRendering/Web/Handlers/PathHandler.cs
===================================================================
--- binary-improvements2/MapRendering/Web/Handlers/PathHandler.cs	(revision 374)
+++ 	(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 374)
+++ 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 374)
+++ 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 374)
+++ 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 374)
+++ 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 ();
