/// <summary> /// Process a request for a resource. /// </summary> /// <param name="Target">The URL the resource is located at.</param> /// <param name="Context"></param> public void ProcessResource(string Target, ContextProvider Context) { RequestProvider Request = Context.Request; ResponseProvider Response = Context.Response; string wwwroot = Config.GetValue("WebserverSettings.wwwroot"); Target = wwwroot + Target; //If target is '/', send index.html if it exists if (Target == wwwroot + "/" && File.Exists(wwwroot + "/index.html")) { Target += "index.html"; } //If the file doesn't exist, send a 404 if (!WebFiles.WebPages.Contains(Target) || !File.Exists(Target)) { Log.Warning("Refused request for " + Request.Url.LocalPath.ToLower() + ": Not Found"); Response.Send(Utils.GetErrorPage(HttpStatusCode.NotFound), HttpStatusCode.NotFound, "text/html"); return; } //Content-type header shouldn't be set for resources if (Request.ContentType != null) { Log.Warning("Refused request for resource " + Target + ": Unsupported Media Type (" + Request.ContentType + ")"); Response.Send(Utils.GetErrorPage(HttpStatusCode.UnsupportedMediaType), HttpStatusCode.UnsupportedMediaType, "text/html"); return; } //Switch to the request's HTTP method switch (Request.HttpMethod) { case HttpMethod.GET: //Send the resource to the client. Content type will be set according to the resource's file extension. Response.Send(File.ReadAllBytes(Target), HttpStatusCode.OK, Path.GetExtension(Target).ToString() switch { ".css" => "text/css", ".png" => "image/png", ".js" => "text/javascript", ".jpg" => "image/jpeg", ".jpeg" => "image/jpeg", _ => "text/html" }); return;
/// <summary> /// Start this RequestWorker. Meant to run in a separate thread. /// </summary> public void Run() { Log.Info(Thread.CurrentThread.Name + " now running."); do { ContextProvider Context = Queue.Take(); Stopwatch S = new Stopwatch(); S.Start(); RequestProvider Request = Context.Request; ResponseProvider Response = Context.Response; Log.Info("Processing " + Request.HttpMethod + " request for " + Request.Url.LocalPath); #region Redirect //Resolve redirects, if any string URL = Redirect.Resolve(Request.Url.LocalPath.ToLower()); //If null is returned, an infinite loop was detected. if (URL == null) { Log.Error("Couldn't resolve URL; infinite redirection loop. URL: " + Request.Url.LocalPath.ToLower()); Response.Send(Utils.GetErrorPage(HttpStatusCode.LoopDetected, "An infinite loop was detected while trying to access the specified URL."), HttpStatusCode.LoopDetected); continue; } //Remove trailing / if (URL.EndsWith('/') && URL.Length > 1) { URL = URL.Remove(URL.Length - 1); } //Redirect if necessary if (URL != Request.Url.LocalPath.ToLower()) { Log.Info("Request redirected to " + URL); Response.Redirect(URL); Response.Send(HttpStatusCode.PermanentRedirect); continue; } #endregion //Find this request's target Type T = FindEndpoint(Request); if (T != null) { ProcessEndpoint(T, Context); } else { //No endpoint was found, so see if a resource exists at this address instead. ProcessResource(Request.Url.LocalPath.ToLower(), Context); } long TimeSpent = S.ElapsedMilliseconds; Log.Info("Operation complete. Took " + TimeSpent + "ms"); if (TimeSpent >= 250) { Log.Warning("An operation took too long to complete. Took " + TimeSpent + " ms, should be less than 250ms"); } RequestTimeWatcher?.Update(TimeSpent); Listener.QueueSizeWatcher?.Update(RequestWorker.Queue.Count); } while (!Debug || Queue.Count != 0); //Connection.Close(); }
public void ProcessEndpoint(Type T, ContextProvider Context) { RequestProvider Request = Context.Request; ResponseProvider Response = Context.Response; //Create an instance of the specified endpoint and set the required values. //We're not using a constructor because it would require each individual endpoint to have its own constructor that just calls the base APIEndpoint constructor, and that's a pain. APIEndpoint Endpoint = (APIEndpoint)Activator.CreateInstance(T); Endpoint.Connection = Connection; Endpoint.Context = Context; Endpoint.Response = Context.Response; Endpoint.Request = Context.Request; Endpoint.Params = Context.Request.Params; //Set access control headers for CORS support. List <string> AllowedMethods = new List <string>(); foreach (MethodInfo M in T.GetMethods()) { if (M.DeclaringType == GetType()) { AllowedMethods.Add(M.Name); } } bool OriginHeader = Request.Headers.TryGetValue("origin", out List <string> Origins); if (OriginHeader && Program.CORSAddresses.Contains(Origins[0] + '/')) { Response.Headers.Add("Access-Control-Allow-Origin", Origins[0]); } Response.Headers.Add("Allow", string.Join(", ", AllowedMethods)); Response.Headers.Add("Access-Control-Allow-Headers", "Content-Type"); // Get the endpoint method MethodInfo Method = Endpoint.GetType().GetMethod(Request.HttpMethod.ToString()); //Check this method's required content type, if any. RequireContentType CT = Method.GetCustomAttribute <RequireContentType>(); if (CT != null) { if (CT.ContentType == "application/json") { //Try to parse the JSON. If that failed for some reason, check if the response body is actually necessary. //Return null if it isn't, return a 400 Bad Request if it is. using StreamReader Reader = new StreamReader(Request.InputStream, Request.ContentEncoding); string JSONText = Reader.ReadToEnd(); try { Endpoint.JSON = JObject.Parse(JSONText); } catch (JsonReaderException e) { if (Method.GetCustomAttribute <RequireBody>() != null) { Log.Warning("Refused request for endpoint " + T.Name + ": Could not parse JSON"); Log.Debug("Message: " + e.Message); Log.Debug("Received JSON: " + JSONText); Response.Send("Invalid JSON: " + e.Message, HttpStatusCode.BadRequest); return; } Endpoint.JSON = null; } } } //Check whether the user is allowed to use this endpoint method, if necessary. #region PermissionLevel Attribute PermissionLevel Attr = Method.GetCustomAttribute <PermissionLevel>(); if (Attr != null) { //Check cookie. If its missing, send a 401 Unauthorized if the cookie is missing. (because a user MUST be logged in to use an endpoint with a PermissionLevel attribute) Cookie SessionIDCookie = Request.Cookies["SessionID"]; if (SessionIDCookie == null) { Response.Send("No Session", HttpStatusCode.Unauthorized); return; } //Check if a session exists with the Session ID contained in the session cookie. If none is found or the session has expired, send back a 401 Unauthorized Session s = Session.GetUserSession(Connection, SessionIDCookie.Value); if (s == null) { Response.Send("Expired session", HttpStatusCode.Unauthorized); return; } //Renew the session, and save it in the endpoint object. s.Renew(Connection); Endpoint.UserSession = s; //Save user in endpoint object Endpoint.RequestUser = Connection.Get <User>(s.User); //Get department name. If none was found, assume Administrators string DepartmentName; if (Endpoint.Params.ContainsKey("Department")) { DepartmentName = Endpoint.Params["Department"][0]; } else { DepartmentName = "Administrators"; } //Get department. If none was found, send 400 Bad Request Department Dept = Department.GetByName(Connection, DepartmentName); if (Dept == null) { Response.Send("No such Department", HttpStatusCode.BadRequest); return; } //Get permissionlevel PermLevel Level = Endpoint.RequestUser.GetPermissionLevel(Connection, Dept.ID); Endpoint.RequestUserLevel = Level; //Check permission level if (Level < Attr.Level) { Log.Warning("User " + Endpoint.RequestUser.Email + " attempted to access endpoint " + T.Name + " without sufficient permissions"); Log.Warning("Department: '" + DepartmentName + "', User is '" + Level + "' but must be at least '" + Attr.Level + "'"); Response.Send(HttpStatusCode.Forbidden); return; } } #endregion //Attempt to invoke the API endpoint method. If this fails, send a 500 Internal Server Error to the client. try { Method.Invoke(Endpoint, null); } catch (Exception e) { Log.Error("Failed to fullfill request for endpoint " + T.Name + ": " + e.GetType().Name + ", " + e.Message); Response.Send(Utils.GetErrorPage(HttpStatusCode.InternalServerError, e.Message), HttpStatusCode.InternalServerError); } }