tgstation-server 5.12.7
The /tg/station 13 server suite
Loading...
Searching...
No Matches
HomeController.cs
Go to the documentation of this file.
1using System;
2using System.Linq;
3using System.Net;
4using System.Threading;
5using System.Threading.Tasks;
6
7using Microsoft.AspNetCore.Authorization;
8using Microsoft.AspNetCore.Http;
9using Microsoft.AspNetCore.Mvc;
10using Microsoft.EntityFrameworkCore;
11using Microsoft.Extensions.Logging;
12using Microsoft.Extensions.Options;
13using Microsoft.Extensions.Primitives;
14using Microsoft.Net.Http.Headers;
15using Octokit;
16
30
32{
36 [Route(Routes.Root)]
37 public sealed class HomeController : ApiController
38 {
43
48
53
58
63
68
73
78
83
88
93
112 IDatabaseContext databaseContext,
113 IAuthenticationContextFactory authenticationContextFactory,
123 IOptions<GeneralConfiguration> generalConfigurationOptions,
124 IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions,
125 ILogger<HomeController> logger)
126 : base(
127 databaseContext,
128 authenticationContextFactory,
129 logger,
130 false)
131 {
132 this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
133 this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
134 this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
135 this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
136 this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
137 this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
138 this.oAuthProviders = oAuthProviders ?? throw new ArgumentNullException(nameof(oAuthProviders));
139 this.swarmService = swarmService ?? throw new ArgumentNullException(nameof(swarmService));
140 this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
141 generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
142 controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
143 }
144
153 [HttpGet]
154 [AllowAnonymous]
155 [ProducesResponseType(typeof(ServerInformationResponse), 200)]
156#pragma warning disable CA1506
157 public async Task<IActionResult> Home(CancellationToken cancellationToken)
158 {
160 Response.Headers.Add(
161 HeaderNames.Vary,
162 new StringValues(ApiHeaders.ApiVersionHeader));
163
164 if (ApiHeaders == null)
165 {
166 if (controlPanelConfiguration.Enable && !Request.Headers.TryGetValue(ApiHeaders.ApiVersionHeader, out _))
167 {
168 Logger.LogDebug("No API headers on request, redirecting to control panel...");
169
170 var controlPanelRoute = controlPanelConfiguration.PublicPath;
171 if (String.IsNullOrWhiteSpace(controlPanelRoute))
172 controlPanelRoute = ControlPanelController.ControlPanelRoute;
173
174 return Redirect(controlPanelRoute);
175 }
176
177 try
178 {
179 // we only allow authorization header issues
180 var headers = new ApiHeaders(Request.GetTypedHeaders(), true);
181 if (!headers.Compatible())
182 return this.StatusCode(
183 HttpStatusCode.UpgradeRequired,
184 new ErrorMessageResponse(ErrorCode.ApiMismatch));
185 }
186 catch (HeadersException)
187 {
188 return HeadersIssue(true);
189 }
190 }
191
192 return Json(new ServerInformationResponse
193 {
195 ApiVersion = ApiHeaders.Version,
196 DMApiVersion = DMApiConstants.InteropVersion,
197 MinimumPasswordLength = generalConfiguration.MinimumPasswordLength,
198 InstanceLimit = generalConfiguration.InstanceLimit,
200 UserGroupLimit = generalConfiguration.UserGroupLimit,
201 ValidInstancePaths = generalConfiguration.ValidInstancePaths,
202 WindowsHost = platformIdentifier.IsWindows,
203 SwarmServers = swarmService.GetSwarmServers(),
204 OAuthProviderInfos = await oAuthProviders.ProviderInfos(cancellationToken),
205 UpdateInProgress = serverControl.UpdateInProgress,
206 });
207 }
208#pragma warning restore CA1506
209
219 [HttpPost]
220 [ProducesResponseType(typeof(TokenResponse), 200)]
221 [ProducesResponseType(typeof(ErrorMessageResponse), 429)]
222#pragma warning disable CA1506 // TODO: Decomplexify
223 public async Task<IActionResult> CreateToken(CancellationToken cancellationToken)
224 {
225 if (ApiHeaders == null)
226 {
227 Response.Headers.Add(HeaderNames.WWWAuthenticate, new StringValues("basic realm=\"Create TGS bearer token\""));
228 return HeadersIssue(false);
229 }
230
232 return BadRequest(new ErrorMessageResponse(ErrorCode.TokenWithToken));
233
234 var oAuthLogin = ApiHeaders.OAuthProvider.HasValue;
235
236 ISystemIdentity systemIdentity = null;
237 if (!oAuthLogin)
238 try
239 {
240 // trust the system over the database because a user's name can change while still having the same SID
241 systemIdentity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password, cancellationToken);
242 }
243 catch (NotImplementedException ex)
244 {
246 }
247
248 using (systemIdentity)
249 {
250 // Get the user from the database
251 IQueryable<Models.User> query = DatabaseContext.Users.AsQueryable();
252 if (oAuthLogin)
253 {
254 var oAuthProvider = ApiHeaders.OAuthProvider.Value;
255 string externalUserId;
256 try
257 {
258 var validator = oAuthProviders
259 .GetValidator(oAuthProvider);
260
261 if (validator == null)
262 return BadRequest(new ErrorMessageResponse(ErrorCode.OAuthProviderDisabled));
263
264 externalUserId = await validator
265 .ValidateResponseCode(ApiHeaders.Token, cancellationToken);
266
267 Logger.LogTrace("External {oAuthProvider} UID: {externalUserId}", oAuthProvider, externalUserId);
268 }
269 catch (RateLimitExceededException ex)
270 {
271 return RateLimit(ex);
272 }
273
274 if (externalUserId == null)
275 return Unauthorized();
276
277 query = query.Where(
278 x => x.OAuthConnections.Any(
279 y => y.Provider == oAuthProvider
280 && y.ExternalUserId == externalUserId));
281 }
282 else
283 {
284 var canonicalUserName = Models.User.CanonicalizeName(ApiHeaders.Username);
285 if (canonicalUserName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName))
286 return Unauthorized();
287
288 if (systemIdentity == null)
289 query = query.Where(x => x.CanonicalName == canonicalUserName);
290 else
291 query = query.Where(x => x.CanonicalName == canonicalUserName || x.SystemIdentifier == systemIdentity.Uid);
292 }
293
294 var users = await query
295 .Select(x => new Models.User
296 {
297 Id = x.Id,
298 PasswordHash = x.PasswordHash,
299 Enabled = x.Enabled,
300 Name = x.Name,
301 SystemIdentifier = x.SystemIdentifier,
302 })
303 .ToListAsync(cancellationToken);
304
305 // Pick the DB user first
306 var user = users
307 .OrderByDescending(dbUser => dbUser.SystemIdentifier == null)
308 .FirstOrDefault();
309
310 // No user? You're not allowed
311 if (user == null)
312 return Unauthorized();
313
314 // A system user may have had their name AND password changed to one in our DB...
315 // Or a DB user was created that had the same user/pass as a system user
316 // Dumb admins...
317 // FALLBACK TO THE DB USER HERE, DO NOT REVEAL A SYSTEM LOGIN!!!
318 // This of course, allows system users to discover TGS users in this (HIGHLY IMPROBABLE) case but that is not our fault
319 var originalHash = user.PasswordHash;
320 var isLikelyDbUser = originalHash != null;
321 bool usingSystemIdentity = systemIdentity != null && !isLikelyDbUser;
322 if (!oAuthLogin)
323 if (!usingSystemIdentity)
324 {
325 // DB User password check and update
326 if (!isLikelyDbUser || !cryptographySuite.CheckUserPassword(user, ApiHeaders.Password))
327 return Unauthorized();
328 if (user.PasswordHash != originalHash)
329 {
330 Logger.LogDebug("User ID {userId}'s password hash needs a refresh, updating database.", user.Id);
331 var updatedUser = new Models.User
332 {
333 Id = user.Id,
334 };
335 DatabaseContext.Users.Attach(updatedUser);
336 updatedUser.PasswordHash = user.PasswordHash;
337 await DatabaseContext.Save(cancellationToken);
338 }
339 }
340 else
341 {
342 var usernameMismatch = systemIdentity.Username != user.Name;
343 if (isLikelyDbUser || usernameMismatch)
344 {
345 DatabaseContext.Users.Attach(user);
346 if (isLikelyDbUser)
347 {
348 // cleanup from https://github.com/tgstation/tgstation-server/issues/1528
349 Logger.LogDebug("System user ID {userId}'s PasswordHash is polluted, updating database.", user.Id);
350 user.PasswordHash = null;
351 user.LastPasswordUpdate = DateTimeOffset.UtcNow;
352 }
353
354 if (usernameMismatch)
355 {
356 // System identity username change update
357 Logger.LogDebug("User ID {userId}'s system identity needs a refresh, updating database.", user.Id);
358 user.Name = systemIdentity.Username;
359 user.CanonicalName = Models.User.CanonicalizeName(user.Name);
360 }
361
362 await DatabaseContext.Save(cancellationToken);
363 }
364 }
365
366 // Now that the bookeeping is done, tell them to fuck off if necessary
367 if (!user.Enabled.Value)
368 {
369 Logger.LogTrace("Not logging in disabled user {userId}.", user.Id);
370 return Forbid();
371 }
372
373 var token = await tokenFactory.CreateToken(user, oAuthLogin, cancellationToken);
374 if (usingSystemIdentity)
375 {
376 // expire the identity slightly after the auth token in case of lag
377 var identExpiry = token.ExpiresAt;
378 identExpiry += tokenFactory.ValidationParameters.ClockSkew;
379 identExpiry += TimeSpan.FromSeconds(15);
380 identityCache.CacheSystemIdentity(user, systemIdentity, identExpiry);
381 }
382
383 Logger.LogDebug("Successfully logged in user {userId}!", user.Id);
384
385 return Json(token);
386 }
387 }
388#pragma warning restore CA1506
389 }
390}
Represents the header that must be present for every server request.
Definition: ApiHeaders.cs:22
bool IsTokenAuthentication
If the header uses password or TGS JWT authentication.
Definition: ApiHeaders.cs:111
string? Username
The client's username.
Definition: ApiHeaders.cs:96
string? Token
The client's JWT.
Definition: ApiHeaders.cs:91
static readonly Version Version
Get the version of the Api the caller is using.
Definition: ApiHeaders.cs:61
OAuthProvider? OAuthProvider
The Models.OAuthProvider the Token is for, if any.
Definition: ApiHeaders.cs:106
const string ApiVersionHeader
The ApiVersion header key.
Definition: ApiHeaders.cs:26
string? Password
The client's password.
Definition: ApiHeaders.cs:101
Thrown when trying to generate ApiHeaders from Microsoft.AspNetCore.Http.Headers.RequestHeaders fails...
virtual ? long Id
The ID of the entity.
Definition: EntityId.cs:13
uint UserGroupLimit
The maximum number of user groups allowed.
uint MinimumPasswordLength
Minimum length of database user passwords.
ICollection< string >? ValidInstancePaths
Limits the locations instances may be created or attached from.
uint InstanceLimit
The maximum number of Instances allowed.
Represents an error message returned by the server.
Represents a JWT returned by the API.
Definition: TokenResponse.cs:9
Routes to a server actions.
Definition: Routes.cs:9
const string Root
The root controller.
Definition: Routes.cs:13
Constants used for communication with the DMAPI.
static readonly Version InteropVersion
The DMAPI InteropVersion being used.
string PublicPath
The public path to the TGS control panel from a wider network.
Base Controller for API functions.
ApiHeaders ApiHeaders
The Api.ApiHeaders for the operation.
IActionResult HeadersIssue(bool ignoreMissingAuth)
Response for missing/Invalid headers.
ObjectResult RequiresPosixSystemIdentity(NotImplementedException ex)
Generic 501 response.
StatusCodeResult StatusCode(HttpStatusCode statusCode)
Strongly type calls to ControllerBase.StatusCode(int).
ObjectResult RateLimit(RateLimitExceededException rateLimitException)
429 response for a given rateLimitException .
ILogger< ApiController > Logger
The ILogger for the ApiController.
const string ControlPanelRoute
Route to the ControlPanelController.
Root ApiController for the Application.
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the HomeController.
readonly IServerControl serverControl
The IServerControl for the HomeController.
async Task< IActionResult > Home(CancellationToken cancellationToken)
Main page of the Application.
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the HomeController.
readonly IOAuthProviders oAuthProviders
The IOAuthProviders for the HomeController.
async Task< IActionResult > CreateToken(CancellationToken cancellationToken)
Attempt to authenticate a User using ApiController.ApiHeaders.
readonly IIdentityCache identityCache
The IIdentityCache for the HomeController.
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the HomeController.
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the HomeController.
HomeController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, IAssemblyInformationProvider assemblyInformationProvider, IIdentityCache identityCache, IOAuthProviders oAuthProviders, IPlatformIdentifier platformIdentifier, ISwarmService swarmService, IServerControl serverControl, IOptions< GeneralConfiguration > generalConfigurationOptions, IOptions< ControlPanelConfiguration > controlPanelConfigurationOptions, ILogger< HomeController > logger)
Initializes a new instance of the HomeController class.
readonly ControlPanelConfiguration controlPanelConfiguration
The ControlPanelConfiguration for the HomeController.
readonly ISystemIdentityFactory systemIdentityFactory
The ISystemIdentityFactory for the HomeController.
readonly ISwarmService swarmService
The ISwarmService for the HomeController.
readonly ITokenFactory tokenFactory
The ITokenFactory for the HomeController.
Backend abstract implementation of IDatabaseContext.
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext. A Task representing the running operation.
DbSet< User > Users
The Users in the DatabaseContext.
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
bool UpdateInProgress
Whether or not the server is currently updating.
Contains various cryptographic functions.
bool CheckUserPassword(User user, string password)
Checks a given password matches a given user 's User.PasswordHash. This may result in User....
void CacheSystemIdentity(User user, ISystemIdentity systemIdentity, DateTimeOffset expiry)
Keep a user 's systemIdentity alive until an expiry time.
Task< ISystemIdentity > CreateSystemIdentity(User user, CancellationToken cancellationToken)
Create a ISystemIdentity for a given user .
Represents a user on the current global::System.Runtime.InteropServices.OSPlatform.
string Uid
A unique identifier for the user.
Task< TokenResponse > CreateToken(Models.User user, bool oAuth, CancellationToken cancellationToken)
Create a TokenResponse for a given user .
TokenValidationParameters ValidationParameters
The TokenValidationParameters for the ITokenFactory.
IOAuthValidator GetValidator(OAuthProvider oAuthProvider)
Gets the IOAuthValidator for a given oAuthProvider .
Task< Dictionary< OAuthProvider, OAuthProviderInfo > > ProviderInfos(CancellationToken cancellationToken)
Gets a Dictionary<TKey, TValue> of the provider client IDs.
Used for swarm operations. Functions may be no-op based on configuration.
ICollection< SwarmServerResponse > GetSwarmServers()
Gets the list of SwarmServerResponses in the swarm, including the current one.
For identifying the current platform.
bool IsWindows
If the current platform is a Windows platform.
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
Definition: ErrorCode.cs:11