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

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

Added support for webmods to the backend

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