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

Last change on this file since 426 was 426, checked in by alloc, 19 months ago

*Updated web permissions system
*Fixed webpermissions command
*Moved API "webmods" to "mods", also lists non-webmod mods

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