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

Last change on this file since 413 was 412, checked in by alloc, 21 months ago

Web base class updates for vanilla changes for direct integration with new GamePrefs

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