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

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

Switched to use SpaceWizards.HttpListener

File size: 9.3 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 indexPagePath = "/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 private readonly ConnectionHandler connectionHandler;
26
27 private readonly HttpListener listener = new HttpListener ();
28 private readonly Version httpProtocolVersion = new Version(1, 1);
29
30 public Web (string _modInstancePath) {
31 try {
32 int webPort = GamePrefs.GetInt (EnumUtils.Parse<EnumGamePrefs> ("ControlPanelPort"));
33 if (webPort < 1 || webPort > 65533) {
34 Log.Out ("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 ("Webserver not started (needs Windows XP SP2, Server 2003 or later or Mono)");
43 return;
44 }
45
46 // TODO: Read from config
47 bool useStaticCache = false;
48
49 string webfilesFolder = _modInstancePath + "/webserver";
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 ? (AbstractCache)new SimpleCache () : new DirectAccess (),
63 false)
64 );
65
66 RegisterPathHandler ("/session/", new SessionHandler (webfilesFolder, connectionHandler));
67 RegisterPathHandler ("/userstatus", new UserStatusHandler ());
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 MapRendering.MapRendering.GetTileCache (),
77 false,
78 "web.map")
79 );
80 RegisterPathHandler ("/api/", new ApiHandler ());
81 RegisterPathHandler ("/sse/", new SseHandler ());
82
83 listener.Prefixes.Add ($"http://+:{webPort}/");
84 // listener.Prefixes.Add ($"http://[::1]:{webPort}/");
85 listener.Start ();
86 listener.BeginGetContext (HandleRequest, listener);
87
88 SdtdConsole.Instance.RegisterServer (this);
89
90 Log.Out ("Started Webserver on " + webPort);
91 } catch (Exception e) {
92 Log.Error ("Error in Web.ctor: ");
93 Log.Exception (e);
94 }
95 }
96
97 public void RegisterPathHandler (string _urlBasePath, AbsHandler _handler) {
98 foreach (AbsHandler handler in handlers) {
99 if (handler.UrlBasePath == _urlBasePath) {
100 Log.Error ($"Web: Handler for relative path {_urlBasePath} already registerd.");
101 return;
102 }
103 }
104
105 handlers.Add (_handler);
106 _handler.SetBasePathAndParent (this, _urlBasePath);
107 }
108
109 public void Disconnect () {
110 try {
111 listener.Stop ();
112 listener.Close ();
113 } catch (Exception e) {
114 Log.Out ("Error in Web.Disconnect: " + e);
115 }
116 }
117
118 public void Shutdown () {
119 foreach (AbsHandler handler in handlers) {
120 handler.Shutdown ();
121 }
122 }
123
124 public void SendLine (string _line) {
125 connectionHandler.SendLine (_line);
126 }
127
128 public void SendLog (string _formattedMessage, string _plainMessage, string _trace, LogType _type, DateTime _timestamp, long _uptime) {
129 // Do nothing, handled by LogBuffer internally
130 }
131
132 public static bool IsSslRedirected (HttpListenerRequest _req) {
133 string proto = _req.Headers ["X-Forwarded-Proto"];
134 return !string.IsNullOrEmpty (proto) && proto.Equals ("https", StringComparison.OrdinalIgnoreCase);
135 }
136
137#if ENABLE_PROFILER
138 private readonly UnityEngine.Profiling.CustomSampler authSampler = UnityEngine.Profiling.CustomSampler.Create ("Auth");
139 private readonly UnityEngine.Profiling.CustomSampler handlerSampler = UnityEngine.Profiling.CustomSampler.Create ("Handler");
140#endif
141
142 private void HandleRequest (IAsyncResult _result) {
143 HttpListener listenerInstance = (HttpListener)_result.AsyncState;
144 if (!listenerInstance.IsListening) {
145 return;
146 }
147
148 Interlocked.Increment (ref handlingCount);
149 Interlocked.Increment (ref currentHandlers);
150
151#if ENABLE_PROFILER
152 UnityEngine.Profiling.Profiler.BeginThreadProfiling ("AllocsMods", "WebRequest");
153 HttpListenerContext ctx = listenerInstance.EndGetContext (_result);
154 try {
155#else
156 HttpListenerContext ctx = listenerInstance.EndGetContext (_result);
157 listenerInstance.BeginGetContext (HandleRequest, listenerInstance);
158#endif
159 try {
160 HttpListenerRequest request = ctx.Request;
161 HttpListenerResponse response = ctx.Response;
162 response.SendChunked = false;
163
164 response.ProtocolVersion = httpProtocolVersion;
165
166#if ENABLE_PROFILER
167 authSampler.Begin ();
168#endif
169 int permissionLevel = DoAuthentication (request, out WebConnection conn);
170#if ENABLE_PROFILER
171 authSampler.End ();
172#endif
173
174
175 //Log.Out ("Login status: conn!=null: {0}, permissionlevel: {1}", conn != null, permissionLevel);
176
177
178 if (conn != null) {
179 Cookie cookie = new Cookie ("sid", conn.SessionID, "/") {
180 Expired = false,
181 Expires = DateTime.MinValue,
182 HttpOnly = true,
183 Secure = false
184 };
185 response.AppendCookie (cookie);
186 }
187
188 // No game yet -> fail request
189 if (GameManager.Instance.World == null) {
190 response.StatusCode = (int) HttpStatusCode.ServiceUnavailable;
191 return;
192 }
193
194 string requestPath = request.Url.AbsolutePath;
195
196 if (requestPath.Length < 2) {
197 response.Redirect (indexPagePath);
198 return;
199 }
200
201 ApplyPathHandler (requestPath, request, response, conn, permissionLevel);
202
203 } catch (IOException e) {
204 if (e.InnerException is SocketException) {
205 Log.Out ("Error in Web.HandleRequest(): Remote host closed connection: " + e.InnerException.Message);
206 } else {
207 Log.Out ("Error (IO) in Web.HandleRequest(): " + e);
208 }
209 } catch (Exception e) {
210 Log.Error ("Error in Web.HandleRequest(): ");
211 Log.Exception (e);
212 } finally {
213 if (!ctx.Response.SendChunked) {
214 ctx.Response.Close ();
215 }
216 Interlocked.Decrement (ref currentHandlers);
217 }
218#if ENABLE_PROFILER
219 } finally {
220 listenerInstance.BeginGetContext (HandleRequest, listenerInstance);
221 UnityEngine.Profiling.Profiler.EndThreadProfiling ();
222 }
223#endif
224 }
225
226 public void ApplyPathHandler (string _requestPath, HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _con,
227 int _permissionLevel) {
228 for (int i = handlers.Count - 1; i >= 0; i--) {
229 AbsHandler handler = handlers [i];
230
231 if (_requestPath.StartsWith (handler.UrlBasePath)) {
232 if (!handler.IsAuthorizedForHandler (_con, _permissionLevel)) {
233 _resp.StatusCode = (int)HttpStatusCode.Forbidden;
234 if (_con != null) {
235 //Log.Out ("Web.HandleRequest: user '{0}' not allowed to access '{1}'", _con.SteamID, handler.ModuleName);
236 }
237 } else {
238#if ENABLE_PROFILER
239 handlerSampler.Begin ();
240#endif
241 handler.HandleRequest (_requestPath, _req, _resp, _con, _permissionLevel);
242#if ENABLE_PROFILER
243 handlerSampler.End ();
244#endif
245 }
246
247 return;
248 }
249 }
250
251 // Not really relevant for non-debugging purposes:
252 //Log.Out ("Error in Web.HandleRequest(): No handler found for path \"" + _requestPath + "\"");
253 _resp.StatusCode = (int) HttpStatusCode.NotFound;
254 }
255
256 private int DoAuthentication (HttpListenerRequest _req, out WebConnection _con) {
257 _con = null;
258
259 string sessionId = _req.Cookies ["sid"]?.Value;
260
261 IPEndPoint reqRemoteEndPoint = _req.RemoteEndPoint;
262 if (reqRemoteEndPoint == null) {
263 Log.Warning ("No RemoteEndPoint on web request");
264 return guestPermissionLevel;
265 }
266
267 if (!string.IsNullOrEmpty (sessionId)) {
268 _con = connectionHandler.IsLoggedIn (sessionId, reqRemoteEndPoint.Address);
269 if (_con != null) {
270 return GameManager.Instance.adminTools.GetUserPermissionLevel (_con.UserId);
271 }
272 }
273
274 string remoteEndpointString = reqRemoteEndPoint.ToString ();
275
276 if (_req.QueryString ["adminuser"] != null && _req.QueryString ["admintoken"] != null) {
277 WebPermissions.AdminToken admin = WebPermissions.Instance.GetWebAdmin (_req.QueryString ["adminuser"],
278 _req.QueryString ["admintoken"]);
279 if (admin != null) {
280 return admin.permissionLevel;
281 }
282
283 Log.Warning ("Invalid Admintoken used from " + remoteEndpointString);
284 }
285
286 return guestPermissionLevel;
287 }
288
289 public static void SetResponseTextContent (HttpListenerResponse _resp, string _text) {
290 byte[] buf = Encoding.UTF8.GetBytes (_text);
291 _resp.ContentLength64 = buf.Length;
292 _resp.ContentType = "text/html";
293 _resp.ContentEncoding = Encoding.UTF8;
294 _resp.OutputStream.Write (buf, 0, buf.Length);
295 }
296 }
297}
Note: See TracBrowser for help on using the repository browser.