tgstation-server
The /tg/station 13 server suite
ApiController.cs
Go to the documentation of this file.
1 using Microsoft.AspNetCore.Http;
2 using Microsoft.AspNetCore.Mvc;
3 using Microsoft.AspNetCore.Mvc.Filters;
4 using Microsoft.Extensions.Logging;
5 using System;
6 using System.Globalization;
7 using System.Linq;
8 using System.Net;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
14 
15 namespace Tgstation.Server.Host.Controllers
16 {
20  [Produces(ApiHeaders.ApplicationJson)]
21  [Consumes(ApiHeaders.ApplicationJson)]
22  public abstract class ApiController : Controller
23  {
27  protected ApiHeaders ApiHeaders { get; private set; }
28 
32  protected IDatabaseContext DatabaseContext { get; }
33 
38 
42  protected ILogger Logger { get; }
43 
47  protected Models.Instance Instance { get; }
48 
52  readonly bool requireInstance;
53 
57  readonly bool requireHeaders;
58 
67  public ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance, bool requireHeaders)
68  {
69  DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
70  if (authenticationContextFactory == null)
71  throw new ArgumentNullException(nameof(authenticationContextFactory));
72  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
73  AuthenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
75  this.requireInstance = requireInstance;
76  this.requireHeaders = requireHeaders;
77  }
78 
80  public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
81  {
82  //ALL valid token and login requests that match a route go through this function
83  //404 is returned before
84 
85  if (AuthenticationContext != null && AuthenticationContext.User == null)
86  {
87  //valid token, expired password
88  await Unauthorized().ExecuteResultAsync(context).ConfigureAwait(false);
89  return;
90  }
91 
92  //validate the headers
93  try
94  {
95  ApiHeaders = new ApiHeaders(Request.GetTypedHeaders());
96 
97  if (!ApiHeaders.Compatible())
98  {
99  await StatusCode((int)HttpStatusCode.UpgradeRequired, new ErrorMessage
100  {
101  Message = "Provided API version is incompatible with server version!"
102  }).ExecuteResultAsync(context).ConfigureAwait(false);
103  return;
104  }
105 
106  if (requireInstance)
107  {
108  if (!ApiHeaders.InstanceId.HasValue)
109  {
110  await BadRequest(new ErrorMessage { Message = "Missing Instance header!" }).ExecuteResultAsync(context).ConfigureAwait(false);
111  return;
112  }
114  {
115  //accessing an instance they don't have access to or one that's disabled
116  await Forbid().ExecuteResultAsync(context).ConfigureAwait(false);
117  return;
118  }
119  }
120  }
121  catch (InvalidOperationException e)
122  {
123  if (requireHeaders)
124  {
125  await BadRequest(new ErrorMessage { Message = e.Message }).ExecuteResultAsync(context).ConfigureAwait(false);
126  return;
127  }
128  }
129 
130  if (ModelState?.IsValid == false)
131  {
132  var errorMessages = ModelState.SelectMany(x => x.Value.Errors).Select(x => x.ErrorMessage).ToList();
133  //HACK
134  //do some fuckery to remove RequiredAttribute errors
135  for (var I = 0; I < errorMessages.Count; ++I)
136  {
137  var message = errorMessages[I];
138  if (message.StartsWith("The ", StringComparison.Ordinal) && message.EndsWith(" field is required.", StringComparison.Ordinal))
139  {
140  errorMessages.RemoveAt(I);
141  --I;
142  }
143  }
144  if (errorMessages.Count > 0)
145  {
146  await BadRequest(new ErrorMessage { Message = String.Join(Environment.NewLine, errorMessages) }).ExecuteResultAsync(context).ConfigureAwait(false);
147  return;
148  }
149  }
150 
151  if (ApiHeaders != null)
152  Logger.LogDebug("Request made by User ID {0}. Api version: {1}. User-Agent: {2}. Type: {3}. Route {4}{5} to Instance {6}", AuthenticationContext?.User.Id.ToString(CultureInfo.InvariantCulture), ApiHeaders.ApiVersion, ApiHeaders.UserAgent, Request.Method, Request.Path, Request.QueryString, ApiHeaders.InstanceId);
153 
154  try
155  {
156  await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
157  }
158  catch (OperationCanceledException e)
159  {
160  Logger.LogDebug("Request cancelled! Exception: {0}", e);
161  throw;
162  }
163  }
164  }
165 }
ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance, bool requireHeaders)
Construct an ApiController
override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
const string ApplicationJson
TODO: Remove this when https://github.com/dotnet/corefx/pull/26701 makes it into the sdk ...
Definition: ApiHeaders.cs:21
Version ApiVersion
The client&#39;s API version
Definition: ApiHeaders.cs:71
bool Compatible()
Checks if the ApiVersion is compatible with Version
Represents the currently authenticated Api.Models.User
ProductHeaderValue UserAgent
The client&#39;s user agent
Definition: ApiHeaders.cs:66
Represents the header that must be present for every server request
Definition: ApiHeaders.cs:16
string Message
A human readable description of the error
Definition: ErrorMessage.cs:13
readonly bool requireHeaders
If ApiHeaders are required
long Id
The ID of the User
Definition: User.cs:15
readonly bool requireInstance
If IAuthenticationContext.InstanceUser permissions are required to access the ApiController ...
Metadata about a server instance
Definition: Instance.cs:9
IAuthenticationContext CurrentAuthenticationContext
The IAuthenticationContext the IAuthenticationContextFactory created
long InstanceId
The Models.Instance.Id being accessed
Definition: ApiHeaders.cs:61
Represents an error message returned by the server
Definition: ErrorMessage.cs:8