source: binary-improvements2/MapRendering/Web/Web.cs@ 390

Last change on this file since 390 was 387, checked in by alloc, 3 years ago

Big refactoring in Web to pass around a Context instead of a bunch of individual arguments all the time

File size: 9.9 KB
RevLine 
[230]1using System;
2using System.Collections.Generic;
3using System.IO;
4using System.Net.Sockets;
[382]5using Cookie = System.Net.Cookie;
6using HttpStatusCode = System.Net.HttpStatusCode;
7using IPEndPoint = System.Net.IPEndPoint;
[230]8using System.Threading;
[325]9using AllocsFixes.FileCache;
10using AllocsFixes.NetConnections.Servers.Web.Handlers;
[367]11using AllocsFixes.NetConnections.Servers.Web.SSE;
[382]12using SpaceWizards.HttpListener;
[230]13using UnityEngine;
14
[325]15namespace AllocsFixes.NetConnections.Servers.Web {
[230]16 public class Web : IConsoleServer {
[382]17 private const int guestPermissionLevel = 2000;
[384]18 private const string indexPageUrl = "/app";
[382]19
[325]20 public static int handlingCount;
21 public static int currentHandlers;
22 public static long totalHandlingTime = 0;
[382]23 private readonly List<AbsHandler> handlers = new List<AbsHandler> ();
[384]24 public readonly List<WebMod> webMods = new List<WebMod> ();
[382]25 private readonly ConnectionHandler connectionHandler;
26
[367]27 private readonly HttpListener listener = new HttpListener ();
[382]28 private readonly Version httpProtocolVersion = new Version(1, 1);
[230]29
[382]30 public Web (string _modInstancePath) {
[230]31 try {
[351]32 int webPort = GamePrefs.GetInt (EnumUtils.Parse<EnumGamePrefs> ("ControlPanelPort"));
[230]33 if (webPort < 1 || webPort > 65533) {
[244]34 Log.Out ("Webserver not started (ControlPanelPort not within 1-65533)");
[230]35 return;
36 }
[325]37
[382]38 // TODO: Remove once this becomes the default control panel
39 webPort += 2;
40
41 if (!HttpListener.IsSupported) {
42 Log.Out ("Webserver not started (needs Windows XP SP2, Server 2003 or later or Mono)");
[230]43 return;
44 }
45
[244]46 // TODO: Read from config
[367]47 bool useStaticCache = false;
[244]48
[382]49 string webfilesFolder = _modInstancePath + "/webserver";
50 string webfilesFolderLegacy = _modInstancePath + "/weblegacy";
[230]51
[382]52 connectionHandler = new ConnectionHandler ();
53
54 RegisterPathHandler ("/", new RewriteHandler ("/files/"));
[325]55
[382]56 // React virtual routing
57 RegisterPathHandler ("/app", new RewriteHandler ("/files/index.html", true));
[367]58
[382]59 // Legacy web page
60 RegisterPathHandler ("/weblegacy", new StaticHandler (
61 webfilesFolderLegacy,
62 useStaticCache ? (AbstractCache)new SimpleCache () : new DirectAccess (),
63 false)
64 );
65
[384]66 // Do mods relatively early as they should not be requested a lot, unlike the later registrations, especially for API and map tiles
67 RegisterWebMods (useStaticCache);
68
[382]69 RegisterPathHandler ("/session/", new SessionHandler (webfilesFolder, connectionHandler));
[367]70 RegisterPathHandler ("/userstatus", new UserStatusHandler ());
[384]71 RegisterPathHandler ("/sse/", new SseHandler ());
[382]72 RegisterPathHandler ("/files/", new StaticHandler (
[384]73 webfilesFolder,
74 useStaticCache ? (AbstractCache) new SimpleCache () : new DirectAccess (),
75 false)
[244]76 );
[367]77 RegisterPathHandler ("/itemicons/", new ItemIconHandler (true));
78 RegisterPathHandler ("/map/", new StaticHandler (
[384]79 GameIO.GetSaveGameDir () + "/map",
80 MapRendering.MapRendering.GetTileCache (),
81 false,
82 "web.map")
[230]83 );
[367]84 RegisterPathHandler ("/api/", new ApiHandler ());
[230]85
[382]86 listener.Prefixes.Add ($"http://+:{webPort}/");
87 // listener.Prefixes.Add ($"http://[::1]:{webPort}/");
[367]88 listener.Start ();
[382]89 listener.BeginGetContext (HandleRequest, listener);
[230]90
91 SdtdConsole.Instance.RegisterServer (this);
92
[382]93 Log.Out ("Started Webserver on " + webPort);
[230]94 } catch (Exception e) {
[382]95 Log.Error ("Error in Web.ctor: ");
96 Log.Exception (e);
[230]97 }
98 }
99
[382]100 public void RegisterPathHandler (string _urlBasePath, AbsHandler _handler) {
101 foreach (AbsHandler handler in handlers) {
102 if (handler.UrlBasePath == _urlBasePath) {
103 Log.Error ($"Web: Handler for relative path {_urlBasePath} already registerd.");
104 return;
105 }
[367]106 }
107
[382]108 handlers.Add (_handler);
[367]109 _handler.SetBasePathAndParent (this, _urlBasePath);
110 }
111
[384]112 private void RegisterWebMods (bool _useStaticCache) {
113 foreach (Mod mod in ModManager.GetLoadedMods ()) {
114 try {
115 string webModPath = mod.Path + "/WebMod";
116 if (!Directory.Exists (webModPath)) {
117 continue;
118 }
119
120 try {
121 WebMod webMod = new WebMod (this, mod, _useStaticCache);
122 webMods.Add (webMod);
123 } catch (InvalidDataException e) {
124 Log.Error ($"Could not load webmod from mod {mod.ModInfo.Name.Value}: {e.Message}");
125 }
126 } catch (Exception e) {
127 Log.Error ("Failed loading web mods from mod " + mod.ModInfo.Name.Value);
128 Log.Exception (e);
129 }
130 }
131 }
132
[325]133 public void Disconnect () {
134 try {
[367]135 listener.Stop ();
136 listener.Close ();
[325]137 } catch (Exception e) {
138 Log.Out ("Error in Web.Disconnect: " + e);
139 }
140 }
141
[367]142 public void Shutdown () {
[382]143 foreach (AbsHandler handler in handlers) {
144 handler.Shutdown ();
[367]145 }
146 }
147
[351]148 public void SendLine (string _line) {
149 connectionHandler.SendLine (_line);
[325]150 }
151
[369]152 public void SendLog (string _formattedMessage, string _plainMessage, string _trace, LogType _type, DateTime _timestamp, long _uptime) {
[325]153 // Do nothing, handled by LogBuffer internally
154 }
155
[367]156 public static bool IsSslRedirected (HttpListenerRequest _req) {
[351]157 string proto = _req.Headers ["X-Forwarded-Proto"];
[367]158 return !string.IsNullOrEmpty (proto) && proto.Equals ("https", StringComparison.OrdinalIgnoreCase);
[325]159 }
[332]160
161#if ENABLE_PROFILER
[367]162 private readonly UnityEngine.Profiling.CustomSampler authSampler = UnityEngine.Profiling.CustomSampler.Create ("Auth");
163 private readonly UnityEngine.Profiling.CustomSampler handlerSampler = UnityEngine.Profiling.CustomSampler.Create ("Handler");
[332]164#endif
[325]165
[351]166 private void HandleRequest (IAsyncResult _result) {
[382]167 HttpListener listenerInstance = (HttpListener)_result.AsyncState;
168 if (!listenerInstance.IsListening) {
[326]169 return;
170 }
[325]171
[326]172 Interlocked.Increment (ref handlingCount);
173 Interlocked.Increment (ref currentHandlers);
174
[332]175#if ENABLE_PROFILER
[367]176 UnityEngine.Profiling.Profiler.BeginThreadProfiling ("AllocsMods", "WebRequest");
[382]177 HttpListenerContext ctx = listenerInstance.EndGetContext (_result);
[332]178 try {
179#else
[382]180 HttpListenerContext ctx = listenerInstance.EndGetContext (_result);
181 listenerInstance.BeginGetContext (HandleRequest, listenerInstance);
[332]182#endif
[326]183 try {
184 HttpListenerRequest request = ctx.Request;
185 HttpListenerResponse response = ctx.Response;
186 response.SendChunked = false;
[230]187
[382]188 response.ProtocolVersion = httpProtocolVersion;
[230]189
[383]190 // No game yet -> fail request
191 if (GameManager.Instance.World == null) {
192 response.StatusCode = (int) HttpStatusCode.ServiceUnavailable;
193 return;
194 }
195
196 if (request.Url == null) {
197 response.StatusCode = (int) HttpStatusCode.BadRequest;
198 return;
199 }
200
[332]201#if ENABLE_PROFILER
202 authSampler.Begin ();
203#endif
[367]204 int permissionLevel = DoAuthentication (request, out WebConnection conn);
[332]205#if ENABLE_PROFILER
206 authSampler.End ();
207#endif
[244]208
[326]209 //Log.Out ("Login status: conn!=null: {0}, permissionlevel: {1}", conn != null, permissionLevel);
[244]210
[326]211 if (conn != null) {
[367]212 Cookie cookie = new Cookie ("sid", conn.SessionID, "/") {
213 Expired = false,
214 Expires = DateTime.MinValue,
215 HttpOnly = true,
216 Secure = false
217 };
[326]218 response.AppendCookie (cookie);
219 }
[244]220
[382]221 string requestPath = request.Url.AbsolutePath;
222
223 if (requestPath.Length < 2) {
[384]224 response.Redirect (indexPageUrl);
[326]225 return;
226 }
[387]227
228 RequestContext context = new RequestContext (requestPath, request, response, conn, permissionLevel);
[382]229
[387]230 ApplyPathHandler (context);
[230]231
[326]232 } catch (IOException e) {
233 if (e.InnerException is SocketException) {
[382]234 Log.Out ("Error in Web.HandleRequest(): Remote host closed connection: " + e.InnerException.Message);
[326]235 } else {
236 Log.Out ("Error (IO) in Web.HandleRequest(): " + e);
237 }
238 } catch (Exception e) {
[360]239 Log.Error ("Error in Web.HandleRequest(): ");
240 Log.Exception (e);
[326]241 } finally {
[382]242 if (!ctx.Response.SendChunked) {
[326]243 ctx.Response.Close ();
244 }
245 Interlocked.Decrement (ref currentHandlers);
[230]246 }
[332]247#if ENABLE_PROFILER
248 } finally {
[382]249 listenerInstance.BeginGetContext (HandleRequest, listenerInstance);
[367]250 UnityEngine.Profiling.Profiler.EndThreadProfiling ();
[332]251 }
252#endif
[230]253 }
254
[387]255 public void ApplyPathHandler (RequestContext _context) {
[382]256 for (int i = handlers.Count - 1; i >= 0; i--) {
257 AbsHandler handler = handlers [i];
258
[387]259 if (_context.RequestPath.StartsWith (handler.UrlBasePath)) {
260 if (!handler.IsAuthorizedForHandler (_context.Connection, _context.PermissionLevel)) {
261 _context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
262 if (_context.Connection != null) {
[382]263 //Log.Out ("Web.HandleRequest: user '{0}' not allowed to access '{1}'", _con.SteamID, handler.ModuleName);
264 }
265 } else {
266#if ENABLE_PROFILER
267 handlerSampler.Begin ();
268#endif
[387]269 handler.HandleRequest (_context);
[382]270#if ENABLE_PROFILER
271 handlerSampler.End ();
272#endif
273 }
274
275 return;
276 }
277 }
278
279 // Not really relevant for non-debugging purposes:
280 //Log.Out ("Error in Web.HandleRequest(): No handler found for path \"" + _requestPath + "\"");
[387]281 _context.Response.StatusCode = (int) HttpStatusCode.NotFound;
[382]282 }
283
[244]284 private int DoAuthentication (HttpListenerRequest _req, out WebConnection _con) {
285 _con = null;
286
[382]287 string sessionId = _req.Cookies ["sid"]?.Value;
288
289 IPEndPoint reqRemoteEndPoint = _req.RemoteEndPoint;
290 if (reqRemoteEndPoint == null) {
291 Log.Warning ("No RemoteEndPoint on web request");
292 return guestPermissionLevel;
[230]293 }
[382]294
[244]295 if (!string.IsNullOrEmpty (sessionId)) {
[382]296 _con = connectionHandler.IsLoggedIn (sessionId, reqRemoteEndPoint.Address);
297 if (_con != null) {
[369]298 return GameManager.Instance.adminTools.GetUserPermissionLevel (_con.UserId);
[244]299 }
300 }
301
[382]302 string remoteEndpointString = reqRemoteEndPoint.ToString ();
[367]303
[244]304 if (_req.QueryString ["adminuser"] != null && _req.QueryString ["admintoken"] != null) {
[325]305 WebPermissions.AdminToken admin = WebPermissions.Instance.GetWebAdmin (_req.QueryString ["adminuser"],
306 _req.QueryString ["admintoken"]);
[244]307 if (admin != null) {
308 return admin.permissionLevel;
309 }
[325]310
[367]311 Log.Warning ("Invalid Admintoken used from " + remoteEndpointString);
[244]312 }
313
[382]314 return guestPermissionLevel;
[230]315 }
316 }
[325]317}
Note: See TracBrowser for help on using the repository browser.