tgstation-server  4.3.2
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 Serilog.Context;
6 using System;
7 using System.Globalization;
8 using System.Linq;
9 using System.Net;
10 using System.Threading.Tasks;
11 using Tgstation.Server.Api;
15 
16 namespace Tgstation.Server.Host.Controllers
17 {
21  [Produces(ApiHeaders.ApplicationJson)]
22  [ApiController]
23  public abstract class ApiController : Controller
24  {
28  protected ApiHeaders ApiHeaders { get; private set; }
29 
33  protected IDatabaseContext DatabaseContext { get; }
34 
39 
43  protected ILogger Logger { get; }
44 
48  protected Models.Instance Instance { get; }
49 
53  readonly bool requireInstance;
54 
58  readonly bool requireHeaders;
59 
68  public ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance, bool requireHeaders)
69  {
70  DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
71  if (authenticationContextFactory == null)
72  throw new ArgumentNullException(nameof(authenticationContextFactory));
73  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
74  AuthenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
76  this.requireInstance = requireInstance;
77  this.requireHeaders = requireHeaders;
78  }
79 
81  #pragma warning disable CA1506 // TODO: Decomplexify
82  public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
83  {
84  // ALL valid token and login requests that match a route go through this function
85  // 404 is returned before
86  if (AuthenticationContext != null && AuthenticationContext.User == null)
87  {
88  // valid token, expired password
89  await Unauthorized().ExecuteResultAsync(context).ConfigureAwait(false);
90  return;
91  }
92 
93  // validate the headers
94  try
95  {
96  ApiHeaders = new ApiHeaders(Request.GetTypedHeaders());
97 
98  if (!ApiHeaders.Compatible())
99  {
100  await StatusCode(
101  (int)HttpStatusCode.UpgradeRequired,
102  new ErrorMessage(ErrorCode.ApiMismatch))
103  .ExecuteResultAsync(context)
104  .ConfigureAwait(false);
105  return;
106  }
107 
108  if (requireInstance)
109  {
110  if (!ApiHeaders.InstanceId.HasValue)
111  {
112  await BadRequest(new ErrorMessage(ErrorCode.InstanceHeaderRequired)).ExecuteResultAsync(context).ConfigureAwait(false);
113  return;
114  }
115 
117  {
118  // accessing an instance they don't have access to or one that's disabled
119  await Forbid().ExecuteResultAsync(context).ConfigureAwait(false);
120  return;
121  }
122  }
123  }
124  catch (InvalidOperationException e)
125  {
126  if (requireHeaders)
127  {
128  await BadRequest(
129  new ErrorMessage(ErrorCode.BadHeaders)
130  {
131  AdditionalData = e.Message
132  })
133  .ExecuteResultAsync(context)
134  .ConfigureAwait(false);
135  return;
136  }
137  }
138 
139  if (ModelState?.IsValid == false)
140  {
141  var errorMessages = ModelState
142  .SelectMany(x => x.Value.Errors)
143  .Select(x => x.ErrorMessage)
144 
145  // We use RequiredAttributes purely for preventing properties from becoming nullable in the databases
146  // We validate missing required fields in controllers
147  // Unfortunately, we can't remove the whole validator for that as it checks other things like StringLength
148  // This is the best way to deal with it unfortunately
149  .Where(x => !x.EndsWith(" field is required.", StringComparison.Ordinal));
150 
151  if (errorMessages.Any())
152  {
153  await BadRequest(
154  new ErrorMessage(ErrorCode.ModelValidationFailure)
155  {
156  AdditionalData = String.Join(Environment.NewLine, errorMessages)
157  })
158  .ExecuteResultAsync(context).ConfigureAwait(false);
159  return;
160  }
161 
162  ModelState.Clear();
163  }
164 
165  using (ApiHeaders?.InstanceId != null
166  ? LogContext.PushProperty("Instance", ApiHeaders.InstanceId)
167  : null)
168  using (AuthenticationContext != null
169  ? LogContext.PushProperty("User", AuthenticationContext.User.Id)
170  : null)
171  using (LogContext.PushProperty("Request", $"{Request.Method} {Request.Path}"))
172  {
173  if (ApiHeaders != null)
174  Logger.LogDebug(
175  "Starting API Request: Version: {1}. User-Agent: {2}",
176  AuthenticationContext?.User.Id.Value.ToString(CultureInfo.InvariantCulture),
177  ApiHeaders.ApiVersion.Semver(),
179  Request.Method,
180  Request.Path,
181  Request.QueryString,
183  await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
184  }
185  }
186  #pragma warning restore CA1506
187  }
188 }
ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance, bool requireHeaders)
Construct an ApiController
ErrorCode
Types of ErrorMessages that the API may return.
Definition: ErrorCode.cs:10
override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
const string ApplicationJson
TODO: Remove this when we upgrade to .NET Standard 2.1
Definition: ApiHeaders.cs:22
Version ApiVersion
The client's API version
Definition: ApiHeaders.cs:82
bool Compatible()
Checks if the ApiVersion is compatible with Version
Represents the currently authenticated Api.Models.User
Backend abstract implementation of IDatabaseContext
Represents the header that must be present for every server request
Definition: ApiHeaders.cs:17
string RawUserAgent
The client's raw user agent
Definition: ApiHeaders.cs:77
readonly bool requireHeaders
If ApiHeaders are required
long Id
The ID of the User
Definition: User.cs:16
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 instance Models.EntityId.Id being accessed
Definition: ApiHeaders.cs:67
Represents an error message returned by the server
Definition: ErrorMessage.cs:9