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

Last change on this file since 406 was 404, checked in by alloc, 22 months ago

Latest state including reworking to the permissions system

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