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