| 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;
 | 
|---|
| 9 | using AllocsFixes.FileCache;
 | 
|---|
| 10 | using AllocsFixes.NetConnections.Servers.Web.Handlers;
 | 
|---|
| 11 | using UnityEngine;
 | 
|---|
| 12 | using UnityEngine.Profiling;
 | 
|---|
| 13 | 
 | 
|---|
| 14 | namespace AllocsFixes.NetConnections.Servers.Web {
 | 
|---|
| 15 |         public class Web : IConsoleServer {
 | 
|---|
| 16 |                 private const int GUEST_PERMISSION_LEVEL = 2000;
 | 
|---|
| 17 |                 public static int handlingCount;
 | 
|---|
| 18 |                 public static int currentHandlers;
 | 
|---|
| 19 |                 public static long totalHandlingTime = 0;
 | 
|---|
| 20 |                 private readonly HttpListener _listener = new HttpListener ();
 | 
|---|
| 21 |                 private readonly string dataFolder;
 | 
|---|
| 22 |                 private readonly Dictionary<string, PathHandler> handlers = new CaseInsensitiveStringDictionary<PathHandler> ();
 | 
|---|
| 23 |                 private readonly bool useStaticCache;
 | 
|---|
| 24 | 
 | 
|---|
| 25 |                 public ConnectionHandler connectionHandler;
 | 
|---|
| 26 | 
 | 
|---|
| 27 |                 public Web () {
 | 
|---|
| 28 |                         try {
 | 
|---|
| 29 |                                 int webPort = GamePrefs.GetInt (EnumUtils.Parse<EnumGamePrefs> ("ControlPanelPort"));
 | 
|---|
| 30 |                                 if (webPort < 1 || webPort > 65533) {
 | 
|---|
| 31 |                                         Log.Out ("Webserver not started (ControlPanelPort not within 1-65533)");
 | 
|---|
| 32 |                                         return;
 | 
|---|
| 33 |                                 }
 | 
|---|
| 34 | 
 | 
|---|
| 35 |                                 if (!Directory.Exists (Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location) +
 | 
|---|
| 36 |                                                        "/webserver")) {
 | 
|---|
| 37 |                                         Log.Out ("Webserver not started (folder \"webserver\" not found in WebInterface mod folder)");
 | 
|---|
| 38 |                                         return;
 | 
|---|
| 39 |                                 }
 | 
|---|
| 40 | 
 | 
|---|
| 41 |                                 // TODO: Read from config
 | 
|---|
| 42 |                                 useStaticCache = false;
 | 
|---|
| 43 | 
 | 
|---|
| 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 |                                 }
 | 
|---|
| 50 | 
 | 
|---|
| 51 |                                 handlers.Add (
 | 
|---|
| 52 |                                         "/index.htm",
 | 
|---|
| 53 |                                         new SimpleRedirectHandler ("/static/index.html"));
 | 
|---|
| 54 |                                 handlers.Add (
 | 
|---|
| 55 |                                         "/favicon.ico",
 | 
|---|
| 56 |                                         new SimpleRedirectHandler ("/static/favicon.ico"));
 | 
|---|
| 57 |                                 handlers.Add (
 | 
|---|
| 58 |                                         "/session/",
 | 
|---|
| 59 |                                         new SessionHandler (
 | 
|---|
| 60 |                                                 "/session/",
 | 
|---|
| 61 |                                                 dataFolder,
 | 
|---|
| 62 |                                                 this)
 | 
|---|
| 63 |                                 );
 | 
|---|
| 64 |                                 handlers.Add (
 | 
|---|
| 65 |                                         "/userstatus",
 | 
|---|
| 66 |                                         new UserStatusHandler ()
 | 
|---|
| 67 |                                 );
 | 
|---|
| 68 |                                 if (useStaticCache) {
 | 
|---|
| 69 |                                         handlers.Add (
 | 
|---|
| 70 |                                                 "/static/",
 | 
|---|
| 71 |                                                 new StaticHandler (
 | 
|---|
| 72 |                                                         "/static/",
 | 
|---|
| 73 |                                                         dataFolder,
 | 
|---|
| 74 |                                                         new SimpleCache (),
 | 
|---|
| 75 |                                                         false)
 | 
|---|
| 76 |                                         );
 | 
|---|
| 77 |                                 } else {
 | 
|---|
| 78 |                                         handlers.Add (
 | 
|---|
| 79 |                                                 "/static/",
 | 
|---|
| 80 |                                                 new StaticHandler (
 | 
|---|
| 81 |                                                         "/static/",
 | 
|---|
| 82 |                                                         dataFolder,
 | 
|---|
| 83 |                                                         new DirectAccess (),
 | 
|---|
| 84 |                                                         false)
 | 
|---|
| 85 |                                         );
 | 
|---|
| 86 |                                 }
 | 
|---|
| 87 | 
 | 
|---|
| 88 |                                 handlers.Add (
 | 
|---|
| 89 |                                         "/itemicons/",
 | 
|---|
| 90 |                                         new ItemIconHandler (
 | 
|---|
| 91 |                                                 "/itemicons/",
 | 
|---|
| 92 |                                                 true)
 | 
|---|
| 93 |                                 );
 | 
|---|
| 94 | 
 | 
|---|
| 95 |                                 handlers.Add (
 | 
|---|
| 96 |                                         "/map/",
 | 
|---|
| 97 |                                         new StaticHandler (
 | 
|---|
| 98 |                                                 "/map/",
 | 
|---|
| 99 |                                                 GameUtils.GetSaveGameDir () + "/map",
 | 
|---|
| 100 |                                                 MapRendering.MapRendering.GetTileCache (),
 | 
|---|
| 101 |                                                 false,
 | 
|---|
| 102 |                                                 "web.map")
 | 
|---|
| 103 |                                 );
 | 
|---|
| 104 | 
 | 
|---|
| 105 |                                 handlers.Add (
 | 
|---|
| 106 |                                         "/api/",
 | 
|---|
| 107 |                                         new ApiHandler ("/api/")
 | 
|---|
| 108 |                                 );
 | 
|---|
| 109 | 
 | 
|---|
| 110 |                                 connectionHandler = new ConnectionHandler ();
 | 
|---|
| 111 | 
 | 
|---|
| 112 |                                 _listener.Prefixes.Add (string.Format ("http://*:{0}/", webPort + 2));
 | 
|---|
| 113 |                                 _listener.Start ();
 | 
|---|
| 114 | 
 | 
|---|
| 115 |                                 SdtdConsole.Instance.RegisterServer (this);
 | 
|---|
| 116 | 
 | 
|---|
| 117 |                                 _listener.BeginGetContext (HandleRequest, _listener);
 | 
|---|
| 118 | 
 | 
|---|
| 119 |                                 Log.Out ("Started Webserver on " + (webPort + 2));
 | 
|---|
| 120 |                         } catch (Exception e) {
 | 
|---|
| 121 |                                 Log.Out ("Error in Web.ctor: " + e);
 | 
|---|
| 122 |                         }
 | 
|---|
| 123 |                 }
 | 
|---|
| 124 | 
 | 
|---|
| 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 |                 }
 | 
|---|
| 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
 | 
|---|
| 157 | 
 | 
|---|
| 158 |                 private void HandleRequest (IAsyncResult _result) {
 | 
|---|
| 159 |                         if (!_listener.IsListening) {
 | 
|---|
| 160 |                                 return;
 | 
|---|
| 161 |                         }
 | 
|---|
| 162 | 
 | 
|---|
| 163 |                         Interlocked.Increment (ref handlingCount);
 | 
|---|
| 164 |                         Interlocked.Increment (ref currentHandlers);
 | 
|---|
| 165 | 
 | 
|---|
| 166 | //                              MicroStopwatch msw = new MicroStopwatch ();
 | 
|---|
| 167 | #if ENABLE_PROFILER
 | 
|---|
| 168 |                         Profiler.BeginThreadProfiling ("AllocsMods", "WebRequest");
 | 
|---|
| 169 |                         HttpListenerContext ctx = _listener.EndGetContext (_result);
 | 
|---|
| 170 |                         try {
 | 
|---|
| 171 | #else
 | 
|---|
| 172 |                         HttpListenerContext ctx = _listener.EndGetContext (_result);
 | 
|---|
| 173 |                         _listener.BeginGetContext (HandleRequest, _listener);
 | 
|---|
| 174 | #endif
 | 
|---|
| 175 |                         try {
 | 
|---|
| 176 |                                 HttpListenerRequest request = ctx.Request;
 | 
|---|
| 177 |                                 HttpListenerResponse response = ctx.Response;
 | 
|---|
| 178 |                                 response.SendChunked = false;
 | 
|---|
| 179 | 
 | 
|---|
| 180 |                                 response.ProtocolVersion = HttpProtocolVersion;
 | 
|---|
| 181 | 
 | 
|---|
| 182 |                                 WebConnection conn;
 | 
|---|
| 183 | #if ENABLE_PROFILER
 | 
|---|
| 184 |                                 authSampler.Begin ();
 | 
|---|
| 185 | #endif
 | 
|---|
| 186 |                                 int permissionLevel = DoAuthentication (request, out conn);
 | 
|---|
| 187 | #if ENABLE_PROFILER
 | 
|---|
| 188 |                                 authSampler.End ();
 | 
|---|
| 189 | #endif
 | 
|---|
| 190 | 
 | 
|---|
| 191 | 
 | 
|---|
| 192 |                                 //Log.Out ("Login status: conn!=null: {0}, permissionlevel: {1}", conn != null, permissionLevel);
 | 
|---|
| 193 | 
 | 
|---|
| 194 | 
 | 
|---|
| 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 |                                 }
 | 
|---|
| 203 | 
 | 
|---|
| 204 |                                 // No game yet -> fail request
 | 
|---|
| 205 |                                 if (GameManager.Instance.World == null) {
 | 
|---|
| 206 |                                         response.StatusCode = (int) HttpStatusCode.ServiceUnavailable;
 | 
|---|
| 207 |                                         return;
 | 
|---|
| 208 |                                 }
 | 
|---|
| 209 | 
 | 
|---|
| 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);
 | 
|---|
| 220 |                                                                 }
 | 
|---|
| 221 |                                                         } else {
 | 
|---|
| 222 | #if ENABLE_PROFILER
 | 
|---|
| 223 |                                                                 handlerSampler.Begin ();
 | 
|---|
| 224 | #endif
 | 
|---|
| 225 |                                                                 kvp.Value.HandleRequest (request, response, conn, permissionLevel);
 | 
|---|
| 226 | #if ENABLE_PROFILER
 | 
|---|
| 227 |                                                                 handlerSampler.End ();
 | 
|---|
| 228 | #endif
 | 
|---|
| 229 |                                                         }
 | 
|---|
| 230 | 
 | 
|---|
| 231 |                                                         return;
 | 
|---|
| 232 |                                                 }
 | 
|---|
| 233 |                                         }
 | 
|---|
| 234 |                                 }
 | 
|---|
| 235 | 
 | 
|---|
| 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 |                                 }
 | 
|---|
| 252 | 
 | 
|---|
| 253 | //                                      msw.Stop ();
 | 
|---|
| 254 | //                                      totalHandlingTime += msw.ElapsedMicroseconds;
 | 
|---|
| 255 | //                                      Log.Out ("Web.HandleRequest(): Took {0} µs", msw.ElapsedMicroseconds);
 | 
|---|
| 256 |                                 Interlocked.Decrement (ref currentHandlers);
 | 
|---|
| 257 |                         }
 | 
|---|
| 258 | #if ENABLE_PROFILER
 | 
|---|
| 259 |                         } finally {
 | 
|---|
| 260 |                                 _listener.BeginGetContext (HandleRequest, _listener);
 | 
|---|
| 261 |                                 Profiler.EndThreadProfiling ();
 | 
|---|
| 262 |                         }
 | 
|---|
| 263 | #endif
 | 
|---|
| 264 |                 }
 | 
|---|
| 265 | 
 | 
|---|
| 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;
 | 
|---|
| 272 |                         }
 | 
|---|
| 273 | 
 | 
|---|
| 274 |                         if (!string.IsNullOrEmpty (sessionId)) {
 | 
|---|
| 275 |                                 WebConnection con = connectionHandler.IsLoggedIn (sessionId, _req.RemoteEndPoint.Address);
 | 
|---|
| 276 |                                 if (con != null) {
 | 
|---|
| 277 |                                         _con = con;
 | 
|---|
| 278 |                                         return GameManager.Instance.adminTools.GetAdminToolsClientInfo (_con.SteamID.ToString ())
 | 
|---|
| 279 |                                                 .PermissionLevel;
 | 
|---|
| 280 |                                 }
 | 
|---|
| 281 |                         }
 | 
|---|
| 282 | 
 | 
|---|
| 283 |                         if (_req.QueryString ["adminuser"] != null && _req.QueryString ["admintoken"] != null) {
 | 
|---|
| 284 |                                 WebPermissions.AdminToken admin = WebPermissions.Instance.GetWebAdmin (_req.QueryString ["adminuser"],
 | 
|---|
| 285 |                                         _req.QueryString ["admintoken"]);
 | 
|---|
| 286 |                                 if (admin != null) {
 | 
|---|
| 287 |                                         return admin.permissionLevel;
 | 
|---|
| 288 |                                 }
 | 
|---|
| 289 | 
 | 
|---|
| 290 |                                 Log.Warning ("Invalid Admintoken used from " + _req.RemoteEndPoint);
 | 
|---|
| 291 |                         }
 | 
|---|
| 292 | 
 | 
|---|
| 293 |                         if (_req.Url.AbsolutePath.StartsWith ("/session/verify", StringComparison.OrdinalIgnoreCase)) {
 | 
|---|
| 294 |                                 try {
 | 
|---|
| 295 |                                         ulong id = OpenID.Validate (_req);
 | 
|---|
| 296 |                                         if (id > 0) {
 | 
|---|
| 297 |                                                 WebConnection con = connectionHandler.LogIn (id, _req.RemoteEndPoint.Address);
 | 
|---|
| 298 |                                                 _con = con;
 | 
|---|
| 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);
 | 
|---|
| 303 |                                                 return level;
 | 
|---|
| 304 |                                         }
 | 
|---|
| 305 | 
 | 
|---|
| 306 |                                         Log.Out ("Steam OpenID login failed from {0}", _req.RemoteEndPoint.ToString ());
 | 
|---|
| 307 |                                 } catch (Exception e) {
 | 
|---|
| 308 |                                         Log.Error ("Error validating login:");
 | 
|---|
| 309 |                                         Log.Exception (e);
 | 
|---|
| 310 |                                 }
 | 
|---|
| 311 |                         }
 | 
|---|
| 312 | 
 | 
|---|
| 313 |                         return GUEST_PERMISSION_LEVEL;
 | 
|---|
| 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 |         }
 | 
|---|
| 324 | }
 | 
|---|