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

Last change on this file since 389 was 387, checked in by alloc, 2 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
Line 
1using System;
2using System.Collections.Generic;
3using System.IO;
4using System.Net.Sockets;
5using Cookie = System.Net.Cookie;
6using HttpStatusCode = System.Net.HttpStatusCode;
7using IPEndPoint = System.Net.IPEndPoint;
8using System.Threading;
9using AllocsFixes.FileCache;
10using AllocsFixes.NetConnections.Servers.Web.Handlers;
11using AllocsFixes.NetConnections.Servers.Web.SSE;
12using SpaceWizards.HttpListener;
13using UnityEngine;
14
15namespace AllocsFixes.NetConnections.Servers.Web {
16 public class Web : IConsoleServer {
17 private const int guestPermissionLevel = 2000;
18 private const string indexPageUrl = "/app";
19
20 public static int handlingCount;
21 public static int currentHandlers;
22 public static long totalHandlingTime = 0;
23 private readonly List<AbsHandler> handlers = new List<AbsHandler> ();
24 public readonly List<WebMod> webMods = new List<WebMod> ();
25 private readonly ConnectionHandler connectionHandler;
26
27 private readonly HttpListener listener = new HttpListener ();
28 private readonly Version httpProtocolVersion = new Version(1, 1);
29
30 public Web (string _modInstancePath) {
31 try {
32 int webPort = GamePrefs.GetInt (EnumUtils.Parse<EnumGamePrefs> ("ControlPanelPort"));
33 if (webPort < 1 || webPort > 65533) {
34 Log.Out ("Webserver not started (ControlPanelPort not within 1-65533)");
35 return;
36 }
37
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)");
43 return;
44 }
45
46 // TODO: Read from config
47 bool useStaticCache = false;
48
49 string webfilesFolder = _modInstancePath + "/webserver";
50 string webfilesFolderLegacy = _modInstancePath + "/weblegacy";
51
52 connectionHandler = new ConnectionHandler ();
53
54 RegisterPathHandler ("/", new RewriteHandler ("/files/"));
55
56 // React virtual routing
57 RegisterPathHandler ("/app", new RewriteHandler ("/files/index.html", true));
58
59 // Legacy web page
60 RegisterPathHandler ("/weblegacy", new StaticHandler (
61 webfilesFolderLegacy,
62 useStaticCache ? (AbstractCache)new SimpleCache () : new DirectAccess (),
63 false)
64 );
65
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
69 RegisterPathHandler ("/session/", new SessionHandler (webfilesFolder, connectionHandler));
70 RegisterPathHandler ("/userstatus", new UserStatusHandler ());
71 RegisterPathHandler ("/sse/", new SseHandler ());
72 RegisterPathHandler ("/files/", new StaticHandler (
73 webfilesFolder,
74 useStaticCache ? (AbstractCache) new SimpleCache () : new DirectAccess (),
75 false)
76 );
77 RegisterPathHandler ("/itemicons/", new ItemIconHandler (true));
78 RegisterPathHandler ("/map/", new StaticHandler (
79 GameIO.GetSaveGameDir () + "/map",
80 MapRendering.MapRendering.GetTileCache (),
81 false,
82 "web.map")
83 );
84 RegisterPathHandler ("/api/", new ApiHandler ());
85
86 listener.Prefixes.Add ($"http://+:{webPort}/");
87 // listener.Prefixes.Add ($"http://[::1]:{webPort}/");
88 listener.Start ();
89 listener.BeginGetContext (HandleRequest, listener);
90
91 SdtdConsole.Instance.RegisterServer (this);
92
93 Log.Out ("Started Webserver on " + webPort);
94 } catch (Exception e) {
95 Log.Error ("Error in Web.ctor: ");
96 Log.Exception (e);
97 }
98 }
99
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 }
106 }
107
108 handlers.Add (_handler);
109 _handler.SetBasePathAndParent (this, _urlBasePath);
110 }
111
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
133 public void Disconnect () {
134 try {
135 listener.Stop ();
136 listener.Close ();
137 } catch (Exception e) {
138 Log.Out ("Error in Web.Disconnect: " + e);
139 }
140 }
141
142 public void Shutdown () {
143 foreach (AbsHandler handler in handlers) {
144 handler.Shutdown ();
145 }
146 }
147
148 public void SendLine (string _line) {
149 connectionHandler.SendLine (_line);
150 }
151
152 public void SendLog (string _formattedMessage, string _plainMessage, string _trace, LogType _type, DateTime _timestamp, long _uptime) {
153 // Do nothing, handled by LogBuffer internally
154 }
155
156 public static bool IsSslRedirected (HttpListenerRequest _req) {
157 string proto = _req.Headers ["X-Forwarded-Proto"];
158 return !string.IsNullOrEmpty (proto) && proto.Equals ("https", StringComparison.OrdinalIgnoreCase);
159 }
160
161#if ENABLE_PROFILER
162 private readonly UnityEngine.Profiling.CustomSampler authSampler = UnityEngine.Profiling.CustomSampler.Create ("Auth");
163 private readonly UnityEngine.Profiling.CustomSampler handlerSampler = UnityEngine.Profiling.CustomSampler.Create ("Handler");
164#endif
165
166 private void HandleRequest (IAsyncResult _result) {
167 HttpListener listenerInstance = (HttpListener)_result.AsyncState;
168 if (!listenerInstance.IsListening) {
169 return;
170 }
171
172 Interlocked.Increment (ref handlingCount);
173 Interlocked.Increment (ref currentHandlers);
174
175#if ENABLE_PROFILER
176 UnityEngine.Profiling.Profiler.BeginThreadProfiling ("AllocsMods", "WebRequest");
177 HttpListenerContext ctx = listenerInstance.EndGetContext (_result);
178 try {
179#else
180 HttpListenerContext ctx = listenerInstance.EndGetContext (_result);
181 listenerInstance.BeginGetContext (HandleRequest, listenerInstance);
182#endif
183 try {
184 HttpListenerRequest request = ctx.Request;
185 HttpListenerResponse response = ctx.Response;
186 response.SendChunked = false;
187
188 response.ProtocolVersion = httpProtocolVersion;
189
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
201#if ENABLE_PROFILER
202 authSampler.Begin ();
203#endif
204 int permissionLevel = DoAuthentication (request, out WebConnection conn);
205#if ENABLE_PROFILER
206 authSampler.End ();
207#endif
208
209 //Log.Out ("Login status: conn!=null: {0}, permissionlevel: {1}", conn != null, permissionLevel);
210
211 if (conn != null) {
212 Cookie cookie = new Cookie ("sid", conn.SessionID, "/") {
213 Expired = false,
214 Expires = DateTime.MinValue,
215 HttpOnly = true,
216 Secure = false
217 };
218 response.AppendCookie (cookie);
219 }
220
221 string requestPath = request.Url.AbsolutePath;
222
223 if (requestPath.Length < 2) {
224 response.Redirect (indexPageUrl);
225 return;
226 }
227
228 RequestContext context = new RequestContext (requestPath, request, response, conn, permissionLevel);
229
230 ApplyPathHandler (context);
231
232 } catch (IOException e) {
233 if (e.InnerException is SocketException) {
234 Log.Out ("Error in Web.HandleRequest(): Remote host closed connection: " + e.InnerException.Message);
235 } else {
236 Log.Out ("Error (IO) in Web.HandleRequest(): " + e);
237 }
238 } catch (Exception e) {
239 Log.Error ("Error in Web.HandleRequest(): ");
240 Log.Exception (e);
241 } finally {
242 if (!ctx.Response.SendChunked) {
243 ctx.Response.Close ();
244 }
245 Interlocked.Decrement (ref currentHandlers);
246 }
247#if ENABLE_PROFILER
248 } finally {
249 listenerInstance.BeginGetContext (HandleRequest, listenerInstance);
250 UnityEngine.Profiling.Profiler.EndThreadProfiling ();
251 }
252#endif
253 }
254
255 public void ApplyPathHandler (RequestContext _context) {
256 for (int i = handlers.Count - 1; i >= 0; i--) {
257 AbsHandler handler = handlers [i];
258
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) {
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 (_context);
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 _context.Response.StatusCode = (int) HttpStatusCode.NotFound;
282 }
283
284 private int DoAuthentication (HttpListenerRequest _req, out WebConnection _con) {
285 _con = null;
286
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;
293 }
294
295 if (!string.IsNullOrEmpty (sessionId)) {
296 _con = connectionHandler.IsLoggedIn (sessionId, reqRemoteEndPoint.Address);
297 if (_con != null) {
298 return GameManager.Instance.adminTools.GetUserPermissionLevel (_con.UserId);
299 }
300 }
301
302 string remoteEndpointString = reqRemoteEndPoint.ToString ();
303
304 if (_req.QueryString ["adminuser"] != null && _req.QueryString ["admintoken"] != null) {
305 WebPermissions.AdminToken admin = WebPermissions.Instance.GetWebAdmin (_req.QueryString ["adminuser"],
306 _req.QueryString ["admintoken"]);
307 if (admin != null) {
308 return admin.permissionLevel;
309 }
310
311 Log.Warning ("Invalid Admintoken used from " + remoteEndpointString);
312 }
313
314 return guestPermissionLevel;
315 }
316 }
317}
Note: See TracBrowser for help on using the repository browser.