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

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

Added support for webmods to the backend

File size: 10.2 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.Text;
9using System.Threading;
10using AllocsFixes.FileCache;
11using AllocsFixes.NetConnections.Servers.Web.Handlers;
12using AllocsFixes.NetConnections.Servers.Web.SSE;
13using SpaceWizards.HttpListener;
14using UnityEngine;
15
16namespace AllocsFixes.NetConnections.Servers.Web {
17 public class Web : IConsoleServer {
18 private const int guestPermissionLevel = 2000;
19 private const string indexPageUrl = "/app";
20
21 public static int handlingCount;
22 public static int currentHandlers;
23 public static long totalHandlingTime = 0;
24 private readonly List<AbsHandler> handlers = new List<AbsHandler> ();
25 public readonly List<WebMod> webMods = new List<WebMod> ();
26 private readonly ConnectionHandler connectionHandler;
27
28 private readonly HttpListener listener = new HttpListener ();
29 private readonly Version httpProtocolVersion = new Version(1, 1);
30
31 public Web (string _modInstancePath) {
32 try {
33 int webPort = GamePrefs.GetInt (EnumUtils.Parse<EnumGamePrefs> ("ControlPanelPort"));
34 if (webPort < 1 || webPort > 65533) {
35 Log.Out ("Webserver not started (ControlPanelPort not within 1-65533)");
36 return;
37 }
38
39 // TODO: Remove once this becomes the default control panel
40 webPort += 2;
41
42 if (!HttpListener.IsSupported) {
43 Log.Out ("Webserver not started (needs Windows XP SP2, Server 2003 or later or Mono)");
44 return;
45 }
46
47 // TODO: Read from config
48 bool useStaticCache = false;
49
50 string webfilesFolder = _modInstancePath + "/webserver";
51 string webfilesFolderLegacy = _modInstancePath + "/weblegacy";
52
53 connectionHandler = new ConnectionHandler ();
54
55 RegisterPathHandler ("/", new RewriteHandler ("/files/"));
56
57 // React virtual routing
58 RegisterPathHandler ("/app", new RewriteHandler ("/files/index.html", true));
59
60 // Legacy web page
61 RegisterPathHandler ("/weblegacy", new StaticHandler (
62 webfilesFolderLegacy,
63 useStaticCache ? (AbstractCache)new SimpleCache () : new DirectAccess (),
64 false)
65 );
66
67 // Do mods relatively early as they should not be requested a lot, unlike the later registrations, especially for API and map tiles
68 RegisterWebMods (useStaticCache);
69
70 RegisterPathHandler ("/session/", new SessionHandler (webfilesFolder, connectionHandler));
71 RegisterPathHandler ("/userstatus", new UserStatusHandler ());
72 RegisterPathHandler ("/sse/", new SseHandler ());
73 RegisterPathHandler ("/files/", new StaticHandler (
74 webfilesFolder,
75 useStaticCache ? (AbstractCache) new SimpleCache () : new DirectAccess (),
76 false)
77 );
78 RegisterPathHandler ("/itemicons/", new ItemIconHandler (true));
79 RegisterPathHandler ("/map/", new StaticHandler (
80 GameIO.GetSaveGameDir () + "/map",
81 MapRendering.MapRendering.GetTileCache (),
82 false,
83 "web.map")
84 );
85 RegisterPathHandler ("/api/", new ApiHandler ());
86
87 listener.Prefixes.Add ($"http://+:{webPort}/");
88 // listener.Prefixes.Add ($"http://[::1]:{webPort}/");
89 listener.Start ();
90 listener.BeginGetContext (HandleRequest, listener);
91
92 SdtdConsole.Instance.RegisterServer (this);
93
94 Log.Out ("Started Webserver on " + webPort);
95 } catch (Exception e) {
96 Log.Error ("Error in Web.ctor: ");
97 Log.Exception (e);
98 }
99 }
100
101 public void RegisterPathHandler (string _urlBasePath, AbsHandler _handler) {
102 foreach (AbsHandler handler in handlers) {
103 if (handler.UrlBasePath == _urlBasePath) {
104 Log.Error ($"Web: Handler for relative path {_urlBasePath} already registerd.");
105 return;
106 }
107 }
108
109 handlers.Add (_handler);
110 _handler.SetBasePathAndParent (this, _urlBasePath);
111 }
112
113 private void RegisterWebMods (bool _useStaticCache) {
114 foreach (Mod mod in ModManager.GetLoadedMods ()) {
115 try {
116 string webModPath = mod.Path + "/WebMod";
117 if (!Directory.Exists (webModPath)) {
118 continue;
119 }
120
121 try {
122 WebMod webMod = new WebMod (this, mod, _useStaticCache);
123 webMods.Add (webMod);
124 } catch (InvalidDataException e) {
125 Log.Error ($"Could not load webmod from mod {mod.ModInfo.Name.Value}: {e.Message}");
126 }
127 } catch (Exception e) {
128 Log.Error ("Failed loading web mods from mod " + mod.ModInfo.Name.Value);
129 Log.Exception (e);
130 }
131 }
132 }
133
134 public void Disconnect () {
135 try {
136 listener.Stop ();
137 listener.Close ();
138 } catch (Exception e) {
139 Log.Out ("Error in Web.Disconnect: " + e);
140 }
141 }
142
143 public void Shutdown () {
144 foreach (AbsHandler handler in handlers) {
145 handler.Shutdown ();
146 }
147 }
148
149 public void SendLine (string _line) {
150 connectionHandler.SendLine (_line);
151 }
152
153 public void SendLog (string _formattedMessage, string _plainMessage, string _trace, LogType _type, DateTime _timestamp, long _uptime) {
154 // Do nothing, handled by LogBuffer internally
155 }
156
157 public static bool IsSslRedirected (HttpListenerRequest _req) {
158 string proto = _req.Headers ["X-Forwarded-Proto"];
159 return !string.IsNullOrEmpty (proto) && proto.Equals ("https", StringComparison.OrdinalIgnoreCase);
160 }
161
162#if ENABLE_PROFILER
163 private readonly UnityEngine.Profiling.CustomSampler authSampler = UnityEngine.Profiling.CustomSampler.Create ("Auth");
164 private readonly UnityEngine.Profiling.CustomSampler handlerSampler = UnityEngine.Profiling.CustomSampler.Create ("Handler");
165#endif
166
167 private void HandleRequest (IAsyncResult _result) {
168 HttpListener listenerInstance = (HttpListener)_result.AsyncState;
169 if (!listenerInstance.IsListening) {
170 return;
171 }
172
173 Interlocked.Increment (ref handlingCount);
174 Interlocked.Increment (ref currentHandlers);
175
176#if ENABLE_PROFILER
177 UnityEngine.Profiling.Profiler.BeginThreadProfiling ("AllocsMods", "WebRequest");
178 HttpListenerContext ctx = listenerInstance.EndGetContext (_result);
179 try {
180#else
181 HttpListenerContext ctx = listenerInstance.EndGetContext (_result);
182 listenerInstance.BeginGetContext (HandleRequest, listenerInstance);
183#endif
184 try {
185 HttpListenerRequest request = ctx.Request;
186 HttpListenerResponse response = ctx.Response;
187 response.SendChunked = false;
188
189 response.ProtocolVersion = httpProtocolVersion;
190
191 // No game yet -> fail request
192 if (GameManager.Instance.World == null) {
193 response.StatusCode = (int) HttpStatusCode.ServiceUnavailable;
194 return;
195 }
196
197 if (request.Url == null) {
198 response.StatusCode = (int) HttpStatusCode.BadRequest;
199 return;
200 }
201
202#if ENABLE_PROFILER
203 authSampler.Begin ();
204#endif
205 int permissionLevel = DoAuthentication (request, out WebConnection conn);
206#if ENABLE_PROFILER
207 authSampler.End ();
208#endif
209
210 //Log.Out ("Login status: conn!=null: {0}, permissionlevel: {1}", conn != null, permissionLevel);
211
212 if (conn != null) {
213 Cookie cookie = new Cookie ("sid", conn.SessionID, "/") {
214 Expired = false,
215 Expires = DateTime.MinValue,
216 HttpOnly = true,
217 Secure = false
218 };
219 response.AppendCookie (cookie);
220 }
221
222 string requestPath = request.Url.AbsolutePath;
223
224 if (requestPath.Length < 2) {
225 response.Redirect (indexPageUrl);
226 return;
227 }
228
229 ApplyPathHandler (requestPath, request, response, conn, permissionLevel);
230
231 } catch (IOException e) {
232 if (e.InnerException is SocketException) {
233 Log.Out ("Error in Web.HandleRequest(): Remote host closed connection: " + e.InnerException.Message);
234 } else {
235 Log.Out ("Error (IO) in Web.HandleRequest(): " + e);
236 }
237 } catch (Exception e) {
238 Log.Error ("Error in Web.HandleRequest(): ");
239 Log.Exception (e);
240 } finally {
241 if (!ctx.Response.SendChunked) {
242 ctx.Response.Close ();
243 }
244 Interlocked.Decrement (ref currentHandlers);
245 }
246#if ENABLE_PROFILER
247 } finally {
248 listenerInstance.BeginGetContext (HandleRequest, listenerInstance);
249 UnityEngine.Profiling.Profiler.EndThreadProfiling ();
250 }
251#endif
252 }
253
254 public void ApplyPathHandler (string _requestPath, HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _con,
255 int _permissionLevel) {
256 for (int i = handlers.Count - 1; i >= 0; i--) {
257 AbsHandler handler = handlers [i];
258
259 if (_requestPath.StartsWith (handler.UrlBasePath)) {
260 if (!handler.IsAuthorizedForHandler (_con, _permissionLevel)) {
261 _resp.StatusCode = (int)HttpStatusCode.Forbidden;
262 if (_con != 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 (_requestPath, _req, _resp, _con, _permissionLevel);
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 _resp.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 public static void SetResponseTextContent (HttpListenerResponse _resp, string _text) {
318 byte[] buf = Encoding.UTF8.GetBytes (_text);
319 _resp.ContentLength64 = buf.Length;
320 _resp.ContentType = "text/html";
321 _resp.ContentEncoding = Encoding.UTF8;
322 _resp.OutputStream.Write (buf, 0, buf.Length);
323 }
324 }
325}
Note: See TracBrowser for help on using the repository browser.