source: binary-improvements2/WebServer/src/Web.cs@ 394

Last change on this file since 394 was 394, checked in by alloc, 2 years ago

SessionHandler cleanup + redirect to /app/error/:code
Some profiler usage cleanup

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