[230] | 1 | using System;
|
---|
| 2 | using System.Collections.Generic;
|
---|
| 3 | using System.IO;
|
---|
| 4 | using System.Net;
|
---|
| 5 | using System.Net.Sockets;
|
---|
| 6 | using System.Reflection;
|
---|
| 7 | using System.Text;
|
---|
| 8 | using System.Threading;
|
---|
[325] | 9 | using AllocsFixes.NetConnections.Servers.Web.Handlers;
|
---|
[367] | 10 | using AllocsFixes.NetConnections.Servers.Web.SSE;
|
---|
[230] | 11 | using UnityEngine;
|
---|
[420] | 12 | using Webserver.FileCache;
|
---|
[230] | 13 |
|
---|
[325] | 14 | namespace AllocsFixes.NetConnections.Servers.Web {
|
---|
[230] | 15 | public class Web : IConsoleServer {
|
---|
[244] | 16 | private const int GUEST_PERMISSION_LEVEL = 2000;
|
---|
[325] | 17 | public static int handlingCount;
|
---|
| 18 | public static int currentHandlers;
|
---|
| 19 | public static long totalHandlingTime = 0;
|
---|
[367] | 20 | private readonly HttpListener listener = new HttpListener ();
|
---|
[332] | 21 | private readonly Dictionary<string, PathHandler> handlers = new CaseInsensitiveStringDictionary<PathHandler> ();
|
---|
[230] | 22 |
|
---|
[367] | 23 | public readonly ConnectionHandler connectionHandler;
|
---|
[244] | 24 |
|
---|
[230] | 25 | public Web () {
|
---|
| 26 | try {
|
---|
[420] | 27 | int webPort = GamePrefs.GetInt (EnumUtils.Parse<EnumGamePrefs> (nameof(EnumGamePrefs.WebDashboardPort)));
|
---|
[230] | 28 | if (webPort < 1 || webPort > 65533) {
|
---|
[420] | 29 | Log.Out ("Webserver not started (WebDashboardPort not within 1-65533)");
|
---|
[230] | 30 | return;
|
---|
| 31 | }
|
---|
[325] | 32 |
|
---|
| 33 | if (!Directory.Exists (Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location) +
|
---|
[420] | 34 | "/webserver_legacy")) {
|
---|
| 35 | Log.Out ("Webserver not started (folder \"webserver_legacy\" not found in WebInterface mod folder)");
|
---|
[230] | 36 | return;
|
---|
| 37 | }
|
---|
| 38 |
|
---|
[244] | 39 | // TODO: Read from config
|
---|
[367] | 40 | bool useStaticCache = false;
|
---|
[244] | 41 |
|
---|
[420] | 42 | string dataFolder = Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location) + "/webserver_legacy";
|
---|
[230] | 43 |
|
---|
| 44 | if (!HttpListener.IsSupported) {
|
---|
| 45 | Log.Out ("Webserver not started (needs Windows XP SP2, Server 2003 or later or Mono)");
|
---|
| 46 | return;
|
---|
| 47 | }
|
---|
[325] | 48 |
|
---|
[367] | 49 |
|
---|
| 50 | RegisterPathHandler ("/index.htm", new SimpleRedirectHandler ("/static/index.html"));
|
---|
| 51 | RegisterPathHandler ("/favicon.ico", new SimpleRedirectHandler ("/static/favicon.ico"));
|
---|
| 52 | RegisterPathHandler ("/session/", new SessionHandler (dataFolder));
|
---|
| 53 | RegisterPathHandler ("/userstatus", new UserStatusHandler ());
|
---|
| 54 | RegisterPathHandler ("/static/", new StaticHandler (
|
---|
[325] | 55 | dataFolder,
|
---|
[367] | 56 | useStaticCache ? (AbstractCache) new SimpleCache () : new DirectAccess (),
|
---|
| 57 | false)
|
---|
[244] | 58 | );
|
---|
[367] | 59 | RegisterPathHandler ("/itemicons/", new ItemIconHandler (true));
|
---|
| 60 | RegisterPathHandler ("/map/", new StaticHandler (
|
---|
[369] | 61 | GameIO.GetSaveGameDir () + "/map",
|
---|
[420] | 62 | MapRendering.MapRenderer.GetTileCache (),
|
---|
[244] | 63 | false,
|
---|
| 64 | "web.map")
|
---|
[230] | 65 | );
|
---|
[367] | 66 | RegisterPathHandler ("/api/", new ApiHandler ());
|
---|
| 67 | RegisterPathHandler ("/sse/", new SseHandler ());
|
---|
[230] | 68 |
|
---|
[326] | 69 | connectionHandler = new ConnectionHandler ();
|
---|
[244] | 70 |
|
---|
[367] | 71 | listener.Prefixes.Add ($"http://*:{webPort + 2}/");
|
---|
| 72 | listener.Start ();
|
---|
[230] | 73 |
|
---|
| 74 | SdtdConsole.Instance.RegisterServer (this);
|
---|
| 75 |
|
---|
[367] | 76 | listener.BeginGetContext (HandleRequest, listener);
|
---|
[230] | 77 |
|
---|
[244] | 78 | Log.Out ("Started Webserver on " + (webPort + 2));
|
---|
[230] | 79 | } catch (Exception e) {
|
---|
| 80 | Log.Out ("Error in Web.ctor: " + e);
|
---|
| 81 | }
|
---|
| 82 | }
|
---|
| 83 |
|
---|
[367] | 84 | public void RegisterPathHandler (string _urlBasePath, PathHandler _handler) {
|
---|
| 85 | if (handlers.ContainsKey (_urlBasePath)) {
|
---|
| 86 | Log.Error ($"Web: Handler for relative path {_urlBasePath} already registerd.");
|
---|
| 87 | return;
|
---|
| 88 | }
|
---|
| 89 |
|
---|
| 90 | handlers.Add (_urlBasePath, _handler);
|
---|
| 91 | _handler.SetBasePathAndParent (this, _urlBasePath);
|
---|
| 92 | }
|
---|
| 93 |
|
---|
[325] | 94 | public void Disconnect () {
|
---|
| 95 | try {
|
---|
[367] | 96 | listener.Stop ();
|
---|
| 97 | listener.Close ();
|
---|
[325] | 98 | } catch (Exception e) {
|
---|
| 99 | Log.Out ("Error in Web.Disconnect: " + e);
|
---|
| 100 | }
|
---|
| 101 | }
|
---|
| 102 |
|
---|
[367] | 103 | public void Shutdown () {
|
---|
| 104 | foreach (KeyValuePair<string, PathHandler> kvp in handlers) {
|
---|
| 105 | kvp.Value.Shutdown ();
|
---|
| 106 | }
|
---|
| 107 | }
|
---|
| 108 |
|
---|
[351] | 109 | public void SendLine (string _line) {
|
---|
| 110 | connectionHandler.SendLine (_line);
|
---|
[325] | 111 | }
|
---|
| 112 |
|
---|
[369] | 113 | public void SendLog (string _formattedMessage, string _plainMessage, string _trace, LogType _type, DateTime _timestamp, long _uptime) {
|
---|
[325] | 114 | // Do nothing, handled by LogBuffer internally
|
---|
| 115 | }
|
---|
| 116 |
|
---|
[367] | 117 | public static bool IsSslRedirected (HttpListenerRequest _req) {
|
---|
[351] | 118 | string proto = _req.Headers ["X-Forwarded-Proto"];
|
---|
[367] | 119 | return !string.IsNullOrEmpty (proto) && proto.Equals ("https", StringComparison.OrdinalIgnoreCase);
|
---|
[325] | 120 | }
|
---|
[332] | 121 |
|
---|
| 122 | private readonly Version HttpProtocolVersion = new Version(1, 1);
|
---|
| 123 |
|
---|
| 124 | #if ENABLE_PROFILER
|
---|
[367] | 125 | private readonly UnityEngine.Profiling.CustomSampler authSampler = UnityEngine.Profiling.CustomSampler.Create ("Auth");
|
---|
| 126 | private readonly UnityEngine.Profiling.CustomSampler handlerSampler = UnityEngine.Profiling.CustomSampler.Create ("Handler");
|
---|
[332] | 127 | #endif
|
---|
[325] | 128 |
|
---|
[351] | 129 | private void HandleRequest (IAsyncResult _result) {
|
---|
[367] | 130 | if (!listener.IsListening) {
|
---|
[326] | 131 | return;
|
---|
| 132 | }
|
---|
[325] | 133 |
|
---|
[326] | 134 | Interlocked.Increment (ref handlingCount);
|
---|
| 135 | Interlocked.Increment (ref currentHandlers);
|
---|
| 136 |
|
---|
[282] | 137 | // MicroStopwatch msw = new MicroStopwatch ();
|
---|
[332] | 138 | #if ENABLE_PROFILER
|
---|
[367] | 139 | UnityEngine.Profiling.Profiler.BeginThreadProfiling ("AllocsMods", "WebRequest");
|
---|
[351] | 140 | HttpListenerContext ctx = _listener.EndGetContext (_result);
|
---|
[332] | 141 | try {
|
---|
| 142 | #else
|
---|
[367] | 143 | HttpListenerContext ctx = listener.EndGetContext (_result);
|
---|
| 144 | listener.BeginGetContext (HandleRequest, listener);
|
---|
[332] | 145 | #endif
|
---|
[326] | 146 | try {
|
---|
| 147 | HttpListenerRequest request = ctx.Request;
|
---|
| 148 | HttpListenerResponse response = ctx.Response;
|
---|
| 149 | response.SendChunked = false;
|
---|
[230] | 150 |
|
---|
[332] | 151 | response.ProtocolVersion = HttpProtocolVersion;
|
---|
[230] | 152 |
|
---|
[332] | 153 | #if ENABLE_PROFILER
|
---|
| 154 | authSampler.Begin ();
|
---|
| 155 | #endif
|
---|
[367] | 156 | int permissionLevel = DoAuthentication (request, out WebConnection conn);
|
---|
[332] | 157 | #if ENABLE_PROFILER
|
---|
| 158 | authSampler.End ();
|
---|
| 159 | #endif
|
---|
[244] | 160 |
|
---|
| 161 |
|
---|
[326] | 162 | //Log.Out ("Login status: conn!=null: {0}, permissionlevel: {1}", conn != null, permissionLevel);
|
---|
[244] | 163 |
|
---|
| 164 |
|
---|
[326] | 165 | if (conn != null) {
|
---|
[367] | 166 | Cookie cookie = new Cookie ("sid", conn.SessionID, "/") {
|
---|
| 167 | Expired = false,
|
---|
| 168 | Expires = DateTime.MinValue,
|
---|
| 169 | HttpOnly = true,
|
---|
| 170 | Secure = false
|
---|
| 171 | };
|
---|
[326] | 172 | response.AppendCookie (cookie);
|
---|
| 173 | }
|
---|
[244] | 174 |
|
---|
[326] | 175 | // No game yet -> fail request
|
---|
| 176 | if (GameManager.Instance.World == null) {
|
---|
| 177 | response.StatusCode = (int) HttpStatusCode.ServiceUnavailable;
|
---|
| 178 | return;
|
---|
| 179 | }
|
---|
[286] | 180 |
|
---|
[326] | 181 | if (request.Url.AbsolutePath.Length < 2) {
|
---|
| 182 | handlers ["/index.htm"].HandleRequest (request, response, conn, permissionLevel);
|
---|
| 183 | return;
|
---|
| 184 | } else {
|
---|
| 185 | foreach (KeyValuePair<string, PathHandler> kvp in handlers) {
|
---|
| 186 | if (request.Url.AbsolutePath.StartsWith (kvp.Key)) {
|
---|
| 187 | if (!kvp.Value.IsAuthorizedForHandler (conn, permissionLevel)) {
|
---|
| 188 | response.StatusCode = (int) HttpStatusCode.Forbidden;
|
---|
| 189 | if (conn != null) {
|
---|
| 190 | //Log.Out ("Web.HandleRequest: user '{0}' not allowed to access '{1}'", conn.SteamID, kvp.Value.ModuleName);
|
---|
[230] | 191 | }
|
---|
[326] | 192 | } else {
|
---|
[332] | 193 | #if ENABLE_PROFILER
|
---|
| 194 | handlerSampler.Begin ();
|
---|
| 195 | #endif
|
---|
[326] | 196 | kvp.Value.HandleRequest (request, response, conn, permissionLevel);
|
---|
[332] | 197 | #if ENABLE_PROFILER
|
---|
| 198 | handlerSampler.End ();
|
---|
| 199 | #endif
|
---|
[326] | 200 | }
|
---|
[325] | 201 |
|
---|
[326] | 202 | return;
|
---|
[230] | 203 | }
|
---|
[244] | 204 | }
|
---|
[326] | 205 | }
|
---|
[230] | 206 |
|
---|
[326] | 207 | // Not really relevant for non-debugging purposes:
|
---|
| 208 | //Log.Out ("Error in Web.HandleRequest(): No handler found for path \"" + request.Url.AbsolutePath + "\"");
|
---|
| 209 | response.StatusCode = (int) HttpStatusCode.NotFound;
|
---|
| 210 | } catch (IOException e) {
|
---|
| 211 | if (e.InnerException is SocketException) {
|
---|
| 212 | Log.Out ("Error in Web.HandleRequest(): Remote host closed connection: " +
|
---|
| 213 | e.InnerException.Message);
|
---|
| 214 | } else {
|
---|
| 215 | Log.Out ("Error (IO) in Web.HandleRequest(): " + e);
|
---|
| 216 | }
|
---|
| 217 | } catch (Exception e) {
|
---|
[360] | 218 | Log.Error ("Error in Web.HandleRequest(): ");
|
---|
| 219 | Log.Exception (e);
|
---|
[326] | 220 | } finally {
|
---|
| 221 | if (ctx != null && !ctx.Response.SendChunked) {
|
---|
| 222 | ctx.Response.Close ();
|
---|
| 223 | }
|
---|
[325] | 224 |
|
---|
[282] | 225 | // msw.Stop ();
|
---|
| 226 | // totalHandlingTime += msw.ElapsedMicroseconds;
|
---|
| 227 | // Log.Out ("Web.HandleRequest(): Took {0} µs", msw.ElapsedMicroseconds);
|
---|
[326] | 228 | Interlocked.Decrement (ref currentHandlers);
|
---|
[230] | 229 | }
|
---|
[332] | 230 | #if ENABLE_PROFILER
|
---|
| 231 | } finally {
|
---|
| 232 | _listener.BeginGetContext (HandleRequest, _listener);
|
---|
[367] | 233 | UnityEngine.Profiling.Profiler.EndThreadProfiling ();
|
---|
[332] | 234 | }
|
---|
| 235 | #endif
|
---|
[230] | 236 | }
|
---|
| 237 |
|
---|
[244] | 238 | private int DoAuthentication (HttpListenerRequest _req, out WebConnection _con) {
|
---|
| 239 | _con = null;
|
---|
| 240 |
|
---|
| 241 | string sessionId = null;
|
---|
| 242 | if (_req.Cookies ["sid"] != null) {
|
---|
| 243 | sessionId = _req.Cookies ["sid"].Value;
|
---|
[230] | 244 | }
|
---|
[244] | 245 |
|
---|
| 246 | if (!string.IsNullOrEmpty (sessionId)) {
|
---|
[332] | 247 | WebConnection con = connectionHandler.IsLoggedIn (sessionId, _req.RemoteEndPoint.Address);
|
---|
[244] | 248 | if (con != null) {
|
---|
| 249 | _con = con;
|
---|
[420] | 250 | return GameManager.Instance.adminTools.Users.GetUserPermissionLevel (_con.UserId);
|
---|
[244] | 251 | }
|
---|
| 252 | }
|
---|
| 253 |
|
---|
[367] | 254 | string remoteEndpointString = _req.RemoteEndPoint.ToString ();
|
---|
| 255 |
|
---|
[244] | 256 | if (_req.QueryString ["adminuser"] != null && _req.QueryString ["admintoken"] != null) {
|
---|
[325] | 257 | WebPermissions.AdminToken admin = WebPermissions.Instance.GetWebAdmin (_req.QueryString ["adminuser"],
|
---|
| 258 | _req.QueryString ["admintoken"]);
|
---|
[244] | 259 | if (admin != null) {
|
---|
| 260 | return admin.permissionLevel;
|
---|
| 261 | }
|
---|
[325] | 262 |
|
---|
[367] | 263 | Log.Warning ("Invalid Admintoken used from " + remoteEndpointString);
|
---|
[244] | 264 | }
|
---|
| 265 |
|
---|
[332] | 266 | if (_req.Url.AbsolutePath.StartsWith ("/session/verify", StringComparison.OrdinalIgnoreCase)) {
|
---|
[314] | 267 | try {
|
---|
| 268 | ulong id = OpenID.Validate (_req);
|
---|
| 269 | if (id > 0) {
|
---|
[332] | 270 | WebConnection con = connectionHandler.LogIn (id, _req.RemoteEndPoint.Address);
|
---|
[314] | 271 | _con = con;
|
---|
[420] | 272 | int level = GameManager.Instance.adminTools.Users.GetUserPermissionLevel (con.UserId);
|
---|
[325] | 273 | Log.Out ("Steam OpenID login from {0} with ID {1}, permission level {2}",
|
---|
[369] | 274 | remoteEndpointString, con.UserId, level);
|
---|
[314] | 275 | return level;
|
---|
| 276 | }
|
---|
[325] | 277 |
|
---|
[367] | 278 | Log.Out ("Steam OpenID login failed from {0}", remoteEndpointString);
|
---|
[314] | 279 | } catch (Exception e) {
|
---|
| 280 | Log.Error ("Error validating login:");
|
---|
| 281 | Log.Exception (e);
|
---|
[244] | 282 | }
|
---|
| 283 | }
|
---|
| 284 |
|
---|
| 285 | return GUEST_PERMISSION_LEVEL;
|
---|
[230] | 286 | }
|
---|
| 287 |
|
---|
[351] | 288 | public static void SetResponseTextContent (HttpListenerResponse _resp, string _text) {
|
---|
| 289 | byte[] buf = Encoding.UTF8.GetBytes (_text);
|
---|
| 290 | _resp.ContentLength64 = buf.Length;
|
---|
| 291 | _resp.ContentType = "text/html";
|
---|
| 292 | _resp.ContentEncoding = Encoding.UTF8;
|
---|
| 293 | _resp.OutputStream.Write (buf, 0, buf.Length);
|
---|
[230] | 294 | }
|
---|
| 295 | }
|
---|
[325] | 296 | }
|
---|