Index: binary-improvements/MapRendering/Web/API/Null.cs
===================================================================
--- binary-improvements/MapRendering/Web/API/Null.cs	(revision 360)
+++ binary-improvements/MapRendering/Web/API/Null.cs	(revision 367)
@@ -4,4 +4,7 @@
 namespace AllocsFixes.NetConnections.Servers.Web.API {
 	public class Null : WebAPI {
+		public Null (string _name) : base(_name) {
+		}
+		
 		public override void HandleRequest (HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _user,
 			int _permissionLevel) {
Index: binary-improvements/MapRendering/Web/API/WebAPI.cs
===================================================================
--- binary-improvements/MapRendering/Web/API/WebAPI.cs	(revision 360)
+++ binary-improvements/MapRendering/Web/API/WebAPI.cs	(revision 367)
@@ -2,5 +2,4 @@
 using System.Text;
 using AllocsFixes.JSON;
-using UnityEngine.Profiling;
 
 namespace AllocsFixes.NetConnections.Servers.Web.API {
@@ -8,11 +7,11 @@
 		public readonly string Name;
 
-		protected WebAPI () {
-			Name = GetType ().Name;
+		protected WebAPI (string _name = null) {
+			Name = _name ?? GetType ().Name;
 		}
 
 #if ENABLE_PROFILER
-		private static readonly CustomSampler jsonSerializeSampler = CustomSampler.Create ("JSON_Serialize");
-		private static readonly CustomSampler netWriteSampler = CustomSampler.Create ("JSON_Write");
+		private static readonly UnityEngine.Profiling.CustomSampler jsonSerializeSampler = UnityEngine.Profiling.CustomSampler.Create ("JSON_Serialize");
+		private static readonly UnityEngine.Profiling.CustomSampler netWriteSampler = UnityEngine.Profiling.CustomSampler.Create ("JSON_Write");
 #endif
 
Index: binary-improvements/MapRendering/Web/Handlers/ApiHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/Handlers/ApiHandler.cs	(revision 360)
+++ binary-improvements/MapRendering/Web/Handlers/ApiHandler.cs	(revision 367)
@@ -4,13 +4,10 @@
 using System.Reflection;
 using AllocsFixes.NetConnections.Servers.Web.API;
-using UnityEngine.Profiling;
 
 namespace AllocsFixes.NetConnections.Servers.Web.Handlers {
 	public class ApiHandler : PathHandler {
 		private readonly Dictionary<string, WebAPI> apis = new CaseInsensitiveStringDictionary<WebAPI> ();
-		private readonly string staticPart;
 
-		public ApiHandler (string _staticPart, string _moduleName = null) : base (_moduleName) {
-			staticPart = _staticPart;
+		public ApiHandler (string _moduleName = null) : base (_moduleName) {
 
 			foreach (Type t in Assembly.GetExecutingAssembly ().GetTypes ()) {
@@ -19,45 +16,37 @@
 					if (ctor != null) {
 						WebAPI apiInstance = (WebAPI) ctor.Invoke (new object [0]);
-						addApi (apiInstance.Name, apiInstance);
+						addApi (apiInstance);
 					}
 				}
 			}
 
-			// Add dummy types
-			Type dummy_t = typeof (Null);
-			ConstructorInfo dummy_ctor = dummy_t.GetConstructor (new Type [0]);
-			if (dummy_ctor != null) {
-				WebAPI dummy_apiInstance = (WebAPI) dummy_ctor.Invoke (new object[0]);
-
-				// Permissions that don't map to a real API
-				addApi ("viewallclaims", dummy_apiInstance);
-				addApi ("viewallplayers", dummy_apiInstance);
-			}
+			// Permissions that don't map to a real API
+			addApi (new Null ("viewallclaims"));
+			addApi (new Null ("viewallplayers"));
 		}
 
-		private void addApi (string _apiName, WebAPI _api) {
-			apis.Add (_apiName, _api);
-			WebPermissions.Instance.AddKnownModule ("webapi." + _apiName, _api.DefaultPermissionLevel ());
+		private void addApi (WebAPI _api) {
+			apis.Add (_api.Name, _api);
+			WebPermissions.Instance.AddKnownModule ("webapi." + _api.Name, _api.DefaultPermissionLevel ());
 		}
 
 #if ENABLE_PROFILER
-		private static readonly CustomSampler apiHandlerSampler = CustomSampler.Create ("API_Handler");
+		private static readonly UnityEngine.Profiling.CustomSampler apiHandlerSampler = UnityEngine.Profiling.CustomSampler.Create ("API_Handler");
 #endif
 
 		public override void HandleRequest (HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _user,
 			int _permissionLevel) {
-			string apiName = _req.Url.AbsolutePath.Remove (0, staticPart.Length);
+			string apiName = _req.Url.AbsolutePath.Remove (0, urlBasePath.Length);
 
-			WebAPI api;
-			if (!apis.TryGetValue (apiName, out api)) {
-				Log.Out ("Error in ApiHandler.HandleRequest(): No handler found for API \"" + apiName + "\"");
+			if (!apis.TryGetValue (apiName, out WebAPI api)) {
+				Log.Out ($"Error in {nameof(ApiHandler)}.HandleRequest(): No handler found for API \"{apiName}\"");
 				_resp.StatusCode = (int) HttpStatusCode.NotFound;
 				return;
 			}
 
-			if (!AuthorizeForCommand (apiName, _user, _permissionLevel)) {
+			if (!AuthorizeForApi (apiName, _permissionLevel)) {
 				_resp.StatusCode = (int) HttpStatusCode.Forbidden;
 				if (_user != null) {
-					//Log.Out ("ApiHandler: user '{0}' not allowed to execute '{1}'", user.SteamID, apiName);
+					//Log.Out ($"{nameof(ApiHandler)}: user '{user.SteamID}' not allowed to execute '{apiName}'");
 				}
 
@@ -74,5 +63,5 @@
 #endif
 			} catch (Exception e) {
-				Log.Error ("Error in ApiHandler.HandleRequest(): Handler {0} threw an exception:", api.Name);
+				Log.Error ($"Error in {nameof(ApiHandler)}.HandleRequest(): Handler {api.Name} threw an exception:");
 				Log.Exception (e);
 				_resp.StatusCode = (int) HttpStatusCode.InternalServerError;
@@ -80,5 +69,5 @@
 		}
 
-		private bool AuthorizeForCommand (string _apiName, WebConnection _user, int _permissionLevel) {
+		private bool AuthorizeForApi (string _apiName, 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 360)
+++ binary-improvements/MapRendering/Web/Handlers/ItemIconHandler.cs	(revision 367)
@@ -11,5 +11,4 @@
 		private readonly bool logMissingFiles;
 
-		private readonly string staticPart;
 		private bool loaded;
 
@@ -18,6 +17,5 @@
 		}
 
-		public ItemIconHandler (string _staticPart, bool _logMissingFiles, string _moduleName = null) : base (_moduleName) {
-			staticPart = _staticPart;
+		public ItemIconHandler (bool _logMissingFiles, string _moduleName = null) : base (_moduleName) {
 			logMissingFiles = _logMissingFiles;
 			Instance = this;
@@ -34,5 +32,5 @@
 			}
 
-			string requestFileName = _req.Url.AbsolutePath.Remove (0, staticPart.Length);
+			string requestFileName = _req.Url.AbsolutePath.Remove (0, urlBasePath.Length);
 			requestFileName = requestFileName.Remove (requestFileName.LastIndexOf ('.'));
 
Index: binary-improvements/MapRendering/Web/Handlers/PathHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/Handlers/PathHandler.cs	(revision 360)
+++ binary-improvements/MapRendering/Web/Handlers/PathHandler.cs	(revision 367)
@@ -3,5 +3,7 @@
 namespace AllocsFixes.NetConnections.Servers.Web.Handlers {
 	public abstract class PathHandler {
-		private readonly string moduleName;
+		protected readonly string moduleName;
+		protected string urlBasePath;
+		protected Web parent;
 
 		protected PathHandler (string _moduleName, int _defaultPermissionLevel = 0) {
@@ -24,4 +26,12 @@
 			return true;
 		}
+
+		public virtual void Shutdown () {
+		}
+
+		public virtual void SetBasePathAndParent (Web _parent, string _relativePath) {
+			parent = _parent;
+			urlBasePath = _relativePath;
+		}
 	}
 }
Index: binary-improvements/MapRendering/Web/Handlers/SessionHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/Handlers/SessionHandler.cs	(revision 360)
+++ binary-improvements/MapRendering/Web/Handlers/SessionHandler.cs	(revision 367)
@@ -7,12 +7,6 @@
 		private readonly string footer = "";
 		private readonly string header = "";
-		private readonly Web parent;
-		private readonly string staticPart;
 
-		public SessionHandler (string _staticPart, string _dataFolder, Web _parent, string _moduleName = null) :
-			base (_moduleName) {
-			staticPart = _staticPart;
-			parent = _parent;
-
+		public SessionHandler (string _dataFolder, string _moduleName = null) : base (_moduleName) {
 			if (File.Exists (_dataFolder + "/sessionheader.tmpl")) {
 				header = File.ReadAllText (_dataFolder + "/sessionheader.tmpl");
@@ -26,5 +20,5 @@
 		public override void HandleRequest (HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _user,
 			int _permissionLevel) {
-			string subpath = _req.Url.AbsolutePath.Remove (0, staticPart.Length);
+			string subpath = _req.Url.AbsolutePath.Remove (0, urlBasePath.Length);
 
 			StringBuilder result = new StringBuilder ();
@@ -52,5 +46,5 @@
 					"<h1>Not logged in, <a href=\"/static/index.html\">click to return to main page</a>.</h1>");
 			} else if (subpath.StartsWith ("login")) {
-				string host = (Web.isSslRedirected (_req) ? "https://" : "http://") + _req.UserHostName;
+				string host = (Web.IsSslRedirected (_req) ? "https://" : "http://") + _req.UserHostName;
 				string url = OpenID.GetOpenIdLoginUrl (host, host + "/session/verify");
 				_resp.Redirect (url);
Index: binary-improvements/MapRendering/Web/Handlers/StaticHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/Handlers/StaticHandler.cs	(revision 360)
+++ binary-improvements/MapRendering/Web/Handlers/StaticHandler.cs	(revision 367)
@@ -8,9 +8,7 @@
 		private readonly string datapath;
 		private readonly bool logMissingFiles;
-		private readonly string staticPart;
 
-		public StaticHandler (string _staticPart, string _filePath, AbstractCache _cache, bool _logMissingFiles,
+		public StaticHandler (string _filePath, AbstractCache _cache, bool _logMissingFiles,
 			string _moduleName = null) : base (_moduleName) {
-			staticPart = _staticPart;
 			datapath = _filePath + (_filePath [_filePath.Length - 1] == '/' ? "" : "/");
 			cache = _cache;
@@ -20,5 +18,5 @@
 		public override void HandleRequest (HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _user,
 			int _permissionLevel) {
-			string fn = _req.Url.AbsolutePath.Remove (0, staticPart.Length);
+			string fn = _req.Url.AbsolutePath.Remove (0, urlBasePath.Length);
 
 			byte[] content = cache.GetFileContent (datapath + fn);
Index: binary-improvements/MapRendering/Web/SSE/EventBase.cs
===================================================================
--- binary-improvements/MapRendering/Web/SSE/EventBase.cs	(revision 367)
+++ binary-improvements/MapRendering/Web/SSE/EventBase.cs	(revision 367)
@@ -0,0 +1,137 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using AllocsFixes.JSON;
+
+namespace AllocsFixes.NetConnections.Servers.Web.SSE {
+	public abstract class EventBase {
+		private const int EncodingBufferSize = 1024 * 1024;
+
+		private readonly SseHandler Parent;
+		public readonly string Name;
+
+		private readonly byte[] encodingBuffer;
+		private readonly StringBuilder stringBuilder = new StringBuilder ();
+
+		private readonly List<HttpListenerResponse> openStreams = new List<HttpListenerResponse> ();
+
+		private readonly global::BlockingQueue<(string _eventName, object _data)> sendQueue =
+			new global::BlockingQueue<(string _eventName, object _data)> ();
+
+		private int currentlyOpen;
+		private int totalOpened;
+		private int totalClosed;
+
+		protected EventBase (SseHandler _parent, bool _reuseEncodingBuffer = true, string _name = null) {
+			Name = _name ?? GetType ().Name;
+			Parent = _parent;
+			if (_reuseEncodingBuffer) {
+				encodingBuffer = new byte[EncodingBufferSize];
+			}
+		}
+
+		public virtual void AddListener (HttpListenerResponse _resp) {
+			totalOpened++;
+			currentlyOpen++;
+
+			openStreams.Add (_resp);
+		}
+
+		protected void SendData (string _eventName, object _data) {
+			sendQueue.Enqueue ((_eventName, _data));
+			Parent.SignalSendQueue ();
+		}
+
+
+		public void ProcessSendQueue () {
+			while (sendQueue.HasData ()) {
+				(string eventName, object data) = sendQueue.Dequeue ();
+				
+				stringBuilder.Append ("event: ");
+				stringBuilder.AppendLine (eventName);
+				stringBuilder.Append ("data: ");
+				
+				if (data is string dataString) {
+					stringBuilder.AppendLine (dataString);
+				} else if (data is JSONNode dataJson) {
+					dataJson.ToString (stringBuilder);
+					stringBuilder.AppendLine ("");
+				} else {
+					Log.Error ($"SSE ({Name}): Data is neither string nor JSON.");
+					continue;
+				}
+				
+				stringBuilder.AppendLine ("");
+				string output = stringBuilder.ToString ();
+				stringBuilder.Clear ();
+
+				byte[] buf;
+				int bytesToSend;
+				if (encodingBuffer != null) {
+					buf = encodingBuffer;
+					try {
+						bytesToSend = Encoding.UTF8.GetBytes (output, 0, output.Length, buf, 0);
+					} catch (ArgumentException e) {
+						Log.Error ($"SSE ({Name}): Exception while encoding data for output, most likely exceeding buffer size:");
+						Log.Exception (e);
+						return;
+					}
+				} else {
+					buf = Encoding.UTF8.GetBytes (output);
+					bytesToSend = buf.Length;
+				}
+
+				for (int i = openStreams.Count - 1; i >= 0; i--) {
+					HttpListenerResponse resp = openStreams [i];
+					try {
+						if (resp.OutputStream.CanWrite) {
+							resp.OutputStream.Write (buf, 0, bytesToSend);
+							resp.OutputStream.Flush ();
+						} else {
+							currentlyOpen--;
+							totalClosed++;
+
+							Log.Out (
+								$"SSE ({Name}): Can not write to endpoint, closing. (Left open: {currentlyOpen}, total opened: {totalOpened}, closed: {totalClosed}");
+							openStreams.RemoveAt (i);
+							resp.Close ();
+						}
+					} catch (IOException e) {
+						currentlyOpen--;
+						totalClosed++;
+
+						openStreams.RemoveAt (i);
+
+						if (e.InnerException is SocketException se) {
+							if (se.SocketErrorCode != SocketError.ConnectionAborted) {
+								Log.Error ($"SSE ({Name}): SocketError ({se.SocketErrorCode}) while trying to write: (Left open: {currentlyOpen}, total opened: {totalOpened}, closed: {totalClosed}");
+							}
+						} else {
+							Log.Error (
+								$"SSE ({Name}): IOException while trying to write: (Left open: {currentlyOpen}, total opened: {totalOpened}, closed: {totalClosed}");
+							Log.Exception (e);
+						}
+
+						resp.Close ();
+					} catch (Exception e) {
+						currentlyOpen--;
+						totalClosed++;
+
+						openStreams.RemoveAt (i);
+						Log.Error (
+							$"SSE ({Name}): Exception while trying to write: (Left open: {currentlyOpen}, total opened: {totalOpened}, closed: {totalClosed}");
+						Log.Exception (e);
+						resp.Close ();
+					}
+				}
+			}
+		}
+
+		public virtual int DefaultPermissionLevel () {
+			return 0;
+		}
+	}
+}
Index: binary-improvements/MapRendering/Web/SSE/EventLog.cs
===================================================================
--- binary-improvements/MapRendering/Web/SSE/EventLog.cs	(revision 367)
+++ binary-improvements/MapRendering/Web/SSE/EventLog.cs	(revision 367)
@@ -0,0 +1,50 @@
+using System;
+using System.Net;
+using System.Text;
+using System.Text.RegularExpressions;
+using AllocsFixes.JSON;
+using UnityEngine;
+
+namespace AllocsFixes.NetConnections.Servers.Web.SSE {
+	public class EventLog : EventBase {
+		private static readonly Regex logMessageMatcher =
+			new Regex (@"^([0-9]{4}-[0-9]{2}-[0-9]{2})T([0-9]{2}:[0-9]{2}:[0-9]{2}) ([0-9]+[,.][0-9]+) [A-Z]+ (.*)$");
+
+		public EventLog (SseHandler _parent) : base (_parent, _name: "log") {
+			Logger.Main.LogCallbacks += LogCallback;
+		}
+
+
+		private void LogCallback (string _msg, string _trace, LogType _type) {
+			Match match = logMessageMatcher.Match (_msg);
+
+			string date;
+			string time;
+			string uptime;
+			string message;
+			if (match.Success) {
+				date = match.Groups [1].Value;
+				time = match.Groups [2].Value;
+				uptime = match.Groups [3].Value;
+				message = match.Groups [4].Value;
+			} else {
+				DateTime dt = DateTime.Now;
+				date = $"{dt.Year:0000}-{dt.Month:00}-{dt.Day:00}";
+				time = $"{dt.Hour:00}:{dt.Minute:00}:{dt.Second:00}";
+				uptime = "";
+				message = _msg;
+			}
+
+			JSONObject data = new JSONObject ();
+			data.Add ("msg", new JSONString (message));
+			data.Add ("type", new JSONString (_type.ToStringCached ()));
+			data.Add ("trace", new JSONString (_trace));
+			data.Add ("date", new JSONString (date));
+			data.Add ("time", new JSONString (time));
+			data.Add ("uptime", new JSONString (uptime));
+
+			SendData ("logLine", data);
+		}
+		
+	}
+}
Index: binary-improvements/MapRendering/Web/SSE/SseHandler.cs
===================================================================
--- binary-improvements/MapRendering/Web/SSE/SseHandler.cs	(revision 367)
+++ binary-improvements/MapRendering/Web/SSE/SseHandler.cs	(revision 367)
@@ -0,0 +1,109 @@
+using System;
+using System.Net;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Threading;
+using AllocsFixes.NetConnections.Servers.Web.Handlers;
+
+// Implemented following HTML spec
+// https://html.spec.whatwg.org/multipage/server-sent-events.html
+
+namespace AllocsFixes.NetConnections.Servers.Web.SSE {
+	public class SseHandler : PathHandler {
+		private readonly Dictionary<string, EventBase> events = new CaseInsensitiveStringDictionary<EventBase> ();
+		
+		private ThreadManager.ThreadInfo queueThead;
+		private readonly AutoResetEvent evSendRequest = new AutoResetEvent (false);
+		private bool shutdown;
+
+		public SseHandler (string _moduleName = null) : base (_moduleName) {
+			Type[] ctorTypes = {typeof (SseHandler)};
+			object[] ctorParams = {this};
+
+			foreach (Type t in Assembly.GetExecutingAssembly ().GetTypes ()) {
+				if (!t.IsAbstract && t.IsSubclassOf (typeof (EventBase))) {
+					ConstructorInfo ctor = t.GetConstructor (ctorTypes);
+					if (ctor != null) {
+						EventBase apiInstance = (EventBase) ctor.Invoke (ctorParams);
+						addEvent (apiInstance.Name, apiInstance);
+					}
+				}
+			}
+		}
+
+		public override void SetBasePathAndParent (Web _parent, string _relativePath) {
+			base.SetBasePathAndParent (_parent, _relativePath);
+			
+			queueThead = ThreadManager.StartThread ("SSE-Processing_" + urlBasePath, QueueProcessThread, ThreadPriority.BelowNormal,
+				_useRealThread: true);
+		}
+
+		public override void Shutdown () {
+			base.Shutdown ();
+			shutdown = true;
+			SignalSendQueue ();
+		}
+
+		private 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,
+			int _permissionLevel) {
+			string eventName = _req.Url.AbsolutePath.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;
+				return;
+			}
+
+			if (!AuthorizeForEvent (eventName, _permissionLevel)) {
+				_resp.StatusCode = (int) HttpStatusCode.Forbidden;
+				if (_user != null) {
+					//Log.Out ($"{nameof(SseHandler)}: user '{user.SteamID}' not allowed to access '{eventName}'");
+				}
+
+				return;
+			}
+
+			try {
+				eventInstance.AddListener (_resp);
+
+				// Keep the request open
+				_resp.SendChunked = true;
+
+				_resp.AddHeader ("Content-Type", "text/event-stream");
+				_resp.OutputStream.Flush ();
+			} catch (Exception e) {
+				Log.Error ($"Error in {nameof (SseHandler)}.HandleRequest(): Handler {eventInstance.Name} threw an exception:");
+				Log.Exception (e);
+				_resp.StatusCode = (int) HttpStatusCode.InternalServerError;
+			}
+		}
+
+		private bool AuthorizeForEvent (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);
+
+					foreach (KeyValuePair<string, EventBase> kvp in events) {
+						kvp.Value.ProcessSendQueue ();
+					}
+				}
+			} catch (Exception e) {
+				Log.Error ("SSE: Error processing send queue");
+				Log.Exception (e);
+			}
+		}
+
+		public void SignalSendQueue () {
+			evSendRequest.Set ();
+		}
+	}
+}
Index: binary-improvements/MapRendering/Web/Web.cs
===================================================================
--- binary-improvements/MapRendering/Web/Web.cs	(revision 360)
+++ binary-improvements/MapRendering/Web/Web.cs	(revision 367)
@@ -9,6 +9,6 @@
 using AllocsFixes.FileCache;
 using AllocsFixes.NetConnections.Servers.Web.Handlers;
+using AllocsFixes.NetConnections.Servers.Web.SSE;
 using UnityEngine;
-using UnityEngine.Profiling;
 
 namespace AllocsFixes.NetConnections.Servers.Web {
@@ -18,10 +18,8 @@
 		public static int currentHandlers;
 		public static long totalHandlingTime = 0;
-		private readonly HttpListener _listener = new HttpListener ();
-		private readonly string dataFolder;
+		private readonly HttpListener listener = new HttpListener ();
 		private readonly Dictionary<string, PathHandler> handlers = new CaseInsensitiveStringDictionary<PathHandler> ();
-		private readonly bool useStaticCache;
-
-		public ConnectionHandler connectionHandler;
+
+		public readonly ConnectionHandler connectionHandler;
 
 		public Web () {
@@ -40,7 +38,7 @@
 
 				// TODO: Read from config
-				useStaticCache = false;
-
-				dataFolder = Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location) + "/webserver";
+				bool useStaticCache = false;
+
+				string dataFolder = Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location) + "/webserver";
 
 				if (!HttpListener.IsSupported) {
@@ -49,52 +47,16 @@
 				}
 
-				handlers.Add (
-					"/index.htm",
-					new SimpleRedirectHandler ("/static/index.html"));
-				handlers.Add (
-					"/favicon.ico",
-					new SimpleRedirectHandler ("/static/favicon.ico"));
-				handlers.Add (
-					"/session/",
-					new SessionHandler (
-						"/session/",
+				
+				RegisterPathHandler ("/index.htm", new SimpleRedirectHandler ("/static/index.html"));
+				RegisterPathHandler ("/favicon.ico", new SimpleRedirectHandler ("/static/favicon.ico"));
+				RegisterPathHandler ("/session/", new SessionHandler (dataFolder));
+				RegisterPathHandler ("/userstatus", new UserStatusHandler ());
+				RegisterPathHandler ("/static/", new StaticHandler (
 						dataFolder,
-						this)
+						useStaticCache ? (AbstractCache) new SimpleCache () : new DirectAccess (),
+						false)
 				);
-				handlers.Add (
-					"/userstatus",
-					new UserStatusHandler ()
-				);
-				if (useStaticCache) {
-					handlers.Add (
-						"/static/",
-						new StaticHandler (
-							"/static/",
-							dataFolder,
-							new SimpleCache (),
-							false)
-					);
-				} else {
-					handlers.Add (
-						"/static/",
-						new StaticHandler (
-							"/static/",
-							dataFolder,
-							new DirectAccess (),
-							false)
-					);
-				}
-
-				handlers.Add (
-					"/itemicons/",
-					new ItemIconHandler (
-						"/itemicons/",
-						true)
-				);
-
-				handlers.Add (
-					"/map/",
-					new StaticHandler (
-						"/map/",
+				RegisterPathHandler ("/itemicons/", new ItemIconHandler (true));
+				RegisterPathHandler ("/map/", new StaticHandler (
 						GameUtils.GetSaveGameDir () + "/map",
 						MapRendering.MapRendering.GetTileCache (),
@@ -102,18 +64,15 @@
 						"web.map")
 				);
-
-				handlers.Add (
-					"/api/",
-					new ApiHandler ("/api/")
-				);
+				RegisterPathHandler ("/api/", new ApiHandler ());
+				RegisterPathHandler ("/sse/", new SseHandler ());
 
 				connectionHandler = new ConnectionHandler ();
 
-				_listener.Prefixes.Add (string.Format ("http://*:{0}/", webPort + 2));
-				_listener.Start ();
+				listener.Prefixes.Add ($"http://*:{webPort + 2}/");
+				listener.Start ();
 
 				SdtdConsole.Instance.RegisterServer (this);
 
-				_listener.BeginGetContext (HandleRequest, _listener);
+				listener.BeginGetContext (HandleRequest, listener);
 
 				Log.Out ("Started Webserver on " + (webPort + 2));
@@ -123,8 +82,18 @@
 		}
 
+		public void RegisterPathHandler (string _urlBasePath, PathHandler _handler) {
+			if (handlers.ContainsKey (_urlBasePath)) {
+				Log.Error ($"Web: Handler for relative path {_urlBasePath} already registerd.");
+				return;
+			}
+			
+			handlers.Add (_urlBasePath, _handler);
+			_handler.SetBasePathAndParent (this, _urlBasePath);
+		}
+
 		public void Disconnect () {
 			try {
-				_listener.Stop ();
-				_listener.Close ();
+				listener.Stop ();
+				listener.Close ();
 			} catch (Exception e) {
 				Log.Out ("Error in Web.Disconnect: " + e);
@@ -132,4 +101,10 @@
 		}
 
+		public void Shutdown () {
+			foreach (KeyValuePair<string, PathHandler> kvp in handlers) {
+				kvp.Value.Shutdown ();
+			}
+		}
+
 		public void SendLine (string _line) {
 			connectionHandler.SendLine (_line);
@@ -140,11 +115,7 @@
 		}
 
-		public static bool isSslRedirected (HttpListenerRequest _req) {
+		public static bool IsSslRedirected (HttpListenerRequest _req) {
 			string proto = _req.Headers ["X-Forwarded-Proto"];
-			if (!string.IsNullOrEmpty (proto)) {
-				return proto.Equals ("https", StringComparison.OrdinalIgnoreCase);
-			}
-
-			return false;
+			return !string.IsNullOrEmpty (proto) && proto.Equals ("https", StringComparison.OrdinalIgnoreCase);
 		}
 		
@@ -152,10 +123,10 @@
 		
 #if ENABLE_PROFILER
-		private readonly CustomSampler authSampler = CustomSampler.Create ("Auth");
-		private readonly CustomSampler handlerSampler = CustomSampler.Create ("Handler");
+		private readonly UnityEngine.Profiling.CustomSampler authSampler = UnityEngine.Profiling.CustomSampler.Create ("Auth");
+		private readonly UnityEngine.Profiling.CustomSampler handlerSampler = UnityEngine.Profiling.CustomSampler.Create ("Handler");
 #endif
 
 		private void HandleRequest (IAsyncResult _result) {
-			if (!_listener.IsListening) {
+			if (!listener.IsListening) {
 				return;
 			}
@@ -166,10 +137,10 @@
 //				MicroStopwatch msw = new MicroStopwatch ();
 #if ENABLE_PROFILER
-			Profiler.BeginThreadProfiling ("AllocsMods", "WebRequest");
+			UnityEngine.Profiling.Profiler.BeginThreadProfiling ("AllocsMods", "WebRequest");
 			HttpListenerContext ctx = _listener.EndGetContext (_result);
 			try {
 #else
-			HttpListenerContext ctx = _listener.EndGetContext (_result);
-			_listener.BeginGetContext (HandleRequest, _listener);
+			HttpListenerContext ctx = listener.EndGetContext (_result);
+			listener.BeginGetContext (HandleRequest, listener);
 #endif
 			try {
@@ -180,9 +151,8 @@
 				response.ProtocolVersion = HttpProtocolVersion;
 
-				WebConnection conn;
 #if ENABLE_PROFILER
 				authSampler.Begin ();
 #endif
-				int permissionLevel = DoAuthentication (request, out conn);
+				int permissionLevel = DoAuthentication (request, out WebConnection conn);
 #if ENABLE_PROFILER
 				authSampler.End ();
@@ -194,9 +164,10 @@
 
 				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;
+					Cookie cookie = new Cookie ("sid", conn.SessionID, "/") {
+						Expired = false,
+						Expires = DateTime.MinValue,
+						HttpOnly = true,
+						Secure = false
+					};
 					response.AppendCookie (cookie);
 				}
@@ -260,5 +231,5 @@
 			} finally {
 				_listener.BeginGetContext (HandleRequest, _listener);
-				Profiler.EndThreadProfiling ();
+				UnityEngine.Profiling.Profiler.EndThreadProfiling ();
 			}
 #endif
@@ -281,4 +252,6 @@
 			}
 
+			string remoteEndpointString = _req.RemoteEndPoint.ToString ();
+
 			if (_req.QueryString ["adminuser"] != null && _req.QueryString ["admintoken"] != null) {
 				WebPermissions.AdminToken admin = WebPermissions.Instance.GetWebAdmin (_req.QueryString ["adminuser"],
@@ -288,5 +261,5 @@
 				}
 
-				Log.Warning ("Invalid Admintoken used from " + _req.RemoteEndPoint);
+				Log.Warning ("Invalid Admintoken used from " + remoteEndpointString);
 			}
 
@@ -299,9 +272,9 @@
 						int level = GameManager.Instance.adminTools.GetUserPermissionLevel (id.ToString ());
 						Log.Out ("Steam OpenID login from {0} with ID {1}, permission level {2}",
-							_req.RemoteEndPoint.ToString (), con.SteamID, level);
+							remoteEndpointString, con.SteamID, level);
 						return level;
 					}
 
-					Log.Out ("Steam OpenID login failed from {0}", _req.RemoteEndPoint.ToString ());
+					Log.Out ("Steam OpenID login failed from {0}", remoteEndpointString);
 				} catch (Exception e) {
 					Log.Error ("Error validating login:");
