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