source: binary-improvements/MapRendering/Web/Web.cs@ 367

Last change on this file since 367 was 367, checked in by alloc, 3 years ago

Web:

  • Added SSE (ServerSentEvents) subsystem
  • Added log endpoint to SSE. Less heavy weight and more responsive way of watching the server log
  • Bunch of refactoring
File size: 9.5 KB
Line 
1using System;
2using System.Collections.Generic;
3using System.IO;
4using System.Net;
5using System.Net.Sockets;
6using System.Reflection;
7using System.Text;
8using System.Threading;
9using AllocsFixes.FileCache;
10using AllocsFixes.NetConnections.Servers.Web.Handlers;
11using AllocsFixes.NetConnections.Servers.Web.SSE;
12using UnityEngine;
13
14namespace AllocsFixes.NetConnections.Servers.Web {
15 public class Web : IConsoleServer {
16 private const int GUEST_PERMISSION_LEVEL = 2000;
17 public static int handlingCount;
18 public static int currentHandlers;
19 public static long totalHandlingTime = 0;
20 private readonly HttpListener listener = new HttpListener ();
21 private readonly Dictionary<string, PathHandler> handlers = new CaseInsensitiveStringDictionary<PathHandler> ();
22
23 public readonly ConnectionHandler connectionHandler;
24
25 public Web () {
26 try {
27 int webPort = GamePrefs.GetInt (EnumUtils.Parse<EnumGamePrefs> ("ControlPanelPort"));
28 if (webPort < 1 || webPort > 65533) {
29 Log.Out ("Webserver not started (ControlPanelPort not within 1-65533)");
30 return;
31 }
32
33 if (!Directory.Exists (Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location) +
34 "/webserver")) {
35 Log.Out ("Webserver not started (folder \"webserver\" not found in WebInterface mod folder)");
36 return;
37 }
38
39 // TODO: Read from config
40 bool useStaticCache = false;
41
42 string dataFolder = Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location) + "/webserver";
43
44 if (!HttpListener.IsSupported) {
45 Log.Out ("Webserver not started (needs Windows XP SP2, Server 2003 or later or Mono)");
46 return;
47 }
48
49
50 RegisterPathHandler ("/index.htm", new SimpleRedirectHandler ("/static/index.html"));
51 RegisterPathHandler ("/favicon.ico", new SimpleRedirectHandler ("/static/favicon.ico"));
52 RegisterPathHandler ("/session/", new SessionHandler (dataFolder));
53 RegisterPathHandler ("/userstatus", new UserStatusHandler ());
54 RegisterPathHandler ("/static/", new StaticHandler (
55 dataFolder,
56 useStaticCache ? (AbstractCache) new SimpleCache () : new DirectAccess (),
57 false)
58 );
59 RegisterPathHandler ("/itemicons/", new ItemIconHandler (true));
60 RegisterPathHandler ("/map/", new StaticHandler (
61 GameUtils.GetSaveGameDir () + "/map",
62 MapRendering.MapRendering.GetTileCache (),
63 false,
64 "web.map")
65 );
66 RegisterPathHandler ("/api/", new ApiHandler ());
67 RegisterPathHandler ("/sse/", new SseHandler ());
68
69 connectionHandler = new ConnectionHandler ();
70
71 listener.Prefixes.Add ($"http://*:{webPort + 2}/");
72 listener.Start ();
73
74 SdtdConsole.Instance.RegisterServer (this);
75
76 listener.BeginGetContext (HandleRequest, listener);
77
78 Log.Out ("Started Webserver on " + (webPort + 2));
79 } catch (Exception e) {
80 Log.Out ("Error in Web.ctor: " + e);
81 }
82 }
83
84 public void RegisterPathHandler (string _urlBasePath, PathHandler _handler) {
85 if (handlers.ContainsKey (_urlBasePath)) {
86 Log.Error ($"Web: Handler for relative path {_urlBasePath} already registerd.");
87 return;
88 }
89
90 handlers.Add (_urlBasePath, _handler);
91 _handler.SetBasePathAndParent (this, _urlBasePath);
92 }
93
94 public void Disconnect () {
95 try {
96 listener.Stop ();
97 listener.Close ();
98 } catch (Exception e) {
99 Log.Out ("Error in Web.Disconnect: " + e);
100 }
101 }
102
103 public void Shutdown () {
104 foreach (KeyValuePair<string, PathHandler> kvp in handlers) {
105 kvp.Value.Shutdown ();
106 }
107 }
108
109 public void SendLine (string _line) {
110 connectionHandler.SendLine (_line);
111 }
112
113 public void SendLog (string _text, string _trace, LogType _type) {
114 // Do nothing, handled by LogBuffer internally
115 }
116
117 public static bool IsSslRedirected (HttpListenerRequest _req) {
118 string proto = _req.Headers ["X-Forwarded-Proto"];
119 return !string.IsNullOrEmpty (proto) && proto.Equals ("https", StringComparison.OrdinalIgnoreCase);
120 }
121
122 private readonly Version HttpProtocolVersion = new Version(1, 1);
123
124#if ENABLE_PROFILER
125 private readonly UnityEngine.Profiling.CustomSampler authSampler = UnityEngine.Profiling.CustomSampler.Create ("Auth");
126 private readonly UnityEngine.Profiling.CustomSampler handlerSampler = UnityEngine.Profiling.CustomSampler.Create ("Handler");
127#endif
128
129 private void HandleRequest (IAsyncResult _result) {
130 if (!listener.IsListening) {
131 return;
132 }
133
134 Interlocked.Increment (ref handlingCount);
135 Interlocked.Increment (ref currentHandlers);
136
137// MicroStopwatch msw = new MicroStopwatch ();
138#if ENABLE_PROFILER
139 UnityEngine.Profiling.Profiler.BeginThreadProfiling ("AllocsMods", "WebRequest");
140 HttpListenerContext ctx = _listener.EndGetContext (_result);
141 try {
142#else
143 HttpListenerContext ctx = listener.EndGetContext (_result);
144 listener.BeginGetContext (HandleRequest, listener);
145#endif
146 try {
147 HttpListenerRequest request = ctx.Request;
148 HttpListenerResponse response = ctx.Response;
149 response.SendChunked = false;
150
151 response.ProtocolVersion = HttpProtocolVersion;
152
153#if ENABLE_PROFILER
154 authSampler.Begin ();
155#endif
156 int permissionLevel = DoAuthentication (request, out WebConnection conn);
157#if ENABLE_PROFILER
158 authSampler.End ();
159#endif
160
161
162 //Log.Out ("Login status: conn!=null: {0}, permissionlevel: {1}", conn != null, permissionLevel);
163
164
165 if (conn != null) {
166 Cookie cookie = new Cookie ("sid", conn.SessionID, "/") {
167 Expired = false,
168 Expires = DateTime.MinValue,
169 HttpOnly = true,
170 Secure = false
171 };
172 response.AppendCookie (cookie);
173 }
174
175 // No game yet -> fail request
176 if (GameManager.Instance.World == null) {
177 response.StatusCode = (int) HttpStatusCode.ServiceUnavailable;
178 return;
179 }
180
181 if (request.Url.AbsolutePath.Length < 2) {
182 handlers ["/index.htm"].HandleRequest (request, response, conn, permissionLevel);
183 return;
184 } else {
185 foreach (KeyValuePair<string, PathHandler> kvp in handlers) {
186 if (request.Url.AbsolutePath.StartsWith (kvp.Key)) {
187 if (!kvp.Value.IsAuthorizedForHandler (conn, permissionLevel)) {
188 response.StatusCode = (int) HttpStatusCode.Forbidden;
189 if (conn != null) {
190 //Log.Out ("Web.HandleRequest: user '{0}' not allowed to access '{1}'", conn.SteamID, kvp.Value.ModuleName);
191 }
192 } else {
193#if ENABLE_PROFILER
194 handlerSampler.Begin ();
195#endif
196 kvp.Value.HandleRequest (request, response, conn, permissionLevel);
197#if ENABLE_PROFILER
198 handlerSampler.End ();
199#endif
200 }
201
202 return;
203 }
204 }
205 }
206
207 // Not really relevant for non-debugging purposes:
208 //Log.Out ("Error in Web.HandleRequest(): No handler found for path \"" + request.Url.AbsolutePath + "\"");
209 response.StatusCode = (int) HttpStatusCode.NotFound;
210 } catch (IOException e) {
211 if (e.InnerException is SocketException) {
212 Log.Out ("Error in Web.HandleRequest(): Remote host closed connection: " +
213 e.InnerException.Message);
214 } else {
215 Log.Out ("Error (IO) in Web.HandleRequest(): " + e);
216 }
217 } catch (Exception e) {
218 Log.Error ("Error in Web.HandleRequest(): ");
219 Log.Exception (e);
220 } finally {
221 if (ctx != null && !ctx.Response.SendChunked) {
222 ctx.Response.Close ();
223 }
224
225// msw.Stop ();
226// totalHandlingTime += msw.ElapsedMicroseconds;
227// Log.Out ("Web.HandleRequest(): Took {0} µs", msw.ElapsedMicroseconds);
228 Interlocked.Decrement (ref currentHandlers);
229 }
230#if ENABLE_PROFILER
231 } finally {
232 _listener.BeginGetContext (HandleRequest, _listener);
233 UnityEngine.Profiling.Profiler.EndThreadProfiling ();
234 }
235#endif
236 }
237
238 private int DoAuthentication (HttpListenerRequest _req, out WebConnection _con) {
239 _con = null;
240
241 string sessionId = null;
242 if (_req.Cookies ["sid"] != null) {
243 sessionId = _req.Cookies ["sid"].Value;
244 }
245
246 if (!string.IsNullOrEmpty (sessionId)) {
247 WebConnection con = connectionHandler.IsLoggedIn (sessionId, _req.RemoteEndPoint.Address);
248 if (con != null) {
249 _con = con;
250 return GameManager.Instance.adminTools.GetUserPermissionLevel (_con.SteamID.ToString ());
251 }
252 }
253
254 string remoteEndpointString = _req.RemoteEndPoint.ToString ();
255
256 if (_req.QueryString ["adminuser"] != null && _req.QueryString ["admintoken"] != null) {
257 WebPermissions.AdminToken admin = WebPermissions.Instance.GetWebAdmin (_req.QueryString ["adminuser"],
258 _req.QueryString ["admintoken"]);
259 if (admin != null) {
260 return admin.permissionLevel;
261 }
262
263 Log.Warning ("Invalid Admintoken used from " + remoteEndpointString);
264 }
265
266 if (_req.Url.AbsolutePath.StartsWith ("/session/verify", StringComparison.OrdinalIgnoreCase)) {
267 try {
268 ulong id = OpenID.Validate (_req);
269 if (id > 0) {
270 WebConnection con = connectionHandler.LogIn (id, _req.RemoteEndPoint.Address);
271 _con = con;
272 int level = GameManager.Instance.adminTools.GetUserPermissionLevel (id.ToString ());
273 Log.Out ("Steam OpenID login from {0} with ID {1}, permission level {2}",
274 remoteEndpointString, con.SteamID, level);
275 return level;
276 }
277
278 Log.Out ("Steam OpenID login failed from {0}", remoteEndpointString);
279 } catch (Exception e) {
280 Log.Error ("Error validating login:");
281 Log.Exception (e);
282 }
283 }
284
285 return GUEST_PERMISSION_LEVEL;
286 }
287
288 public static void SetResponseTextContent (HttpListenerResponse _resp, string _text) {
289 byte[] buf = Encoding.UTF8.GetBytes (_text);
290 _resp.ContentLength64 = buf.Length;
291 _resp.ContentType = "text/html";
292 _resp.ContentEncoding = Encoding.UTF8;
293 _resp.OutputStream.Write (buf, 0, buf.Length);
294 }
295 }
296}
Note: See TracBrowser for help on using the repository browser.