tgstation-server 6.12.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
ApiHeaders.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Globalization;
4using System.Linq;
5using System.Net.Http.Headers;
6using System.Net.Mime;
7using System.Reflection;
8using System.Text;
9
10using Microsoft.AspNetCore.Http.Headers;
11using Microsoft.Extensions.Primitives;
12using Microsoft.Net.Http.Headers;
13
18
20{
24 public sealed class ApiHeaders
25 {
29 public const string ApiVersionHeader = "Api";
30
34 public const string InstanceIdHeader = "Instance";
35
39 public const string OAuthProviderHeader = "OAuthProvider";
40
44 public const string BearerAuthenticationScheme = "Bearer";
45
49 public const string BasicAuthenticationScheme = "Basic";
50
54 public const string OAuthAuthenticationScheme = "OAuth";
55
59 public const string ApplicationJsonMime = "application/json";
60
64 const string TextEventStreamMime = "text/event-stream";
65
70
74 static readonly AssemblyName AssemblyName = Assembly.GetExecutingAssembly().GetName();
75
79 static readonly char[] ColonSeparator = [':'];
80
84 public long? InstanceId { get; set; }
85
89 public ProductHeaderValue? UserAgent => ProductInfoHeaderValue.TryParse(RawUserAgent, out var userAgent) ? userAgent.Product : null;
90
94 public string? RawUserAgent { get; }
95
99 public Version ApiVersion { get; }
100
104 public TokenResponse? Token { get; }
105
109 public string? Username { get; }
110
114 public string? Password { get; }
115
119 public string? OAuthCode { get; }
120
125
129 public bool IsTokenAuthentication => Token != null && !OAuthProvider.HasValue;
130
136 public static bool CheckCompatibility(Version otherVersion) => Version.Major == (otherVersion?.Major ?? throw new ArgumentNullException(nameof(otherVersion)));
137
143 public ApiHeaders(ProductHeaderValue userAgent, TokenResponse token)
144 : this(userAgent, token, null, null)
145 {
146 if (userAgent == null)
147 throw new ArgumentNullException(nameof(userAgent));
148 if (token == null)
149 throw new ArgumentNullException(nameof(token));
150 if (token.Bearer == null)
151 throw new InvalidOperationException("token.Bearer must be set!");
152 }
153
160 public ApiHeaders(ProductHeaderValue userAgent, string oAuthCode, OAuthProvider oAuthProvider)
161 : this(userAgent, null, null, null)
162 {
163 if (userAgent == null)
164 throw new ArgumentNullException(nameof(userAgent));
165
166 OAuthCode = oAuthCode ?? throw new ArgumentNullException(nameof(oAuthCode));
167 OAuthProvider = oAuthProvider;
168 }
169
176 public ApiHeaders(ProductHeaderValue userAgent, string username, string password)
177 : this(userAgent, null, username, password)
178 {
179 if (userAgent == null)
180 throw new ArgumentNullException(nameof(userAgent));
181 if (username == null)
182 throw new ArgumentNullException(nameof(username));
183 if (password == null)
184 throw new ArgumentNullException(nameof(password));
185 }
186
194#pragma warning disable CA1502 // TODO: Decomplexify
195 public ApiHeaders(RequestHeaders requestHeaders, bool ignoreMissingAuth, bool allowEventStreamAccept)
196 {
197 if (requestHeaders == null)
198 throw new ArgumentNullException(nameof(requestHeaders));
199
200 var badHeaders = HeaderErrorTypes.None;
201 var errorBuilder = new StringBuilder();
202 var multipleErrors = false;
203 void AddError(HeaderErrorTypes headerType, string message)
204 {
205 if (badHeaders != HeaderErrorTypes.None)
206 {
207 multipleErrors = true;
208 errorBuilder.AppendLine();
209 }
210
211 badHeaders |= headerType;
212 errorBuilder.Append(message);
213 }
214
215 var jsonAccept = new Microsoft.Net.Http.Headers.MediaTypeHeaderValue(ApplicationJsonMime);
216 var eventStreamAccept = new Microsoft.Net.Http.Headers.MediaTypeHeaderValue(TextEventStreamMime);
217 if (!requestHeaders.Accept.Any(accept => accept.IsSubsetOf(jsonAccept)))
218 if (!allowEventStreamAccept)
219 AddError(HeaderErrorTypes.Accept, $"Client does not accept {ApplicationJsonMime}!");
220 else if (!requestHeaders.Accept.Any(eventStreamAccept.IsSubsetOf))
221 AddError(HeaderErrorTypes.Accept, $"Client does not accept {ApplicationJsonMime} or {TextEventStreamMime}!");
222
223 if (!requestHeaders.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgentValues) || userAgentValues.Count == 0)
224 AddError(HeaderErrorTypes.UserAgent, $"Missing {HeaderNames.UserAgent} header!");
225 else
226 {
227 RawUserAgent = userAgentValues.First();
228 if (String.IsNullOrWhiteSpace(RawUserAgent))
229 AddError(HeaderErrorTypes.UserAgent, $"Malformed {HeaderNames.UserAgent} header!");
230 }
231
232 // make sure the api header matches ours
233 Version? apiVersion = null;
234 if (!requestHeaders.Headers.TryGetValue(ApiVersionHeader, out var apiUserAgentHeaderValues) || !ProductInfoHeaderValue.TryParse(apiUserAgentHeaderValues.FirstOrDefault(), out var apiUserAgent) || apiUserAgent.Product.Name != AssemblyName.Name)
235 AddError(HeaderErrorTypes.Api, $"Missing {ApiVersionHeader} header!");
236 else if (!Version.TryParse(apiUserAgent.Product.Version, out apiVersion))
237 AddError(HeaderErrorTypes.Api, $"Malformed {ApiVersionHeader} header!");
238
239 if (!requestHeaders.Headers.TryGetValue(HeaderNames.Authorization, out StringValues authorization))
240 {
241 if (!ignoreMissingAuth)
242 AddError(HeaderErrorTypes.AuthorizationMissing, $"Missing {HeaderNames.Authorization} header!");
243 }
244 else
245 {
246 var auth = authorization.First();
247 var splits = new List<string>(auth.Split(' '));
248 var scheme = splits.First();
249 if (String.IsNullOrWhiteSpace(scheme))
250 AddError(HeaderErrorTypes.AuthorizationInvalid, "Missing authentication scheme!");
251 else
252 {
253 splits.RemoveAt(0);
254 var parameter = String.Concat(splits);
255 if (String.IsNullOrEmpty(parameter))
256 AddError(HeaderErrorTypes.AuthorizationInvalid, "Missing authentication parameter!");
257 else
258 {
259 if (requestHeaders.Headers.TryGetValue(InstanceIdHeader, out var instanceIdValues))
260 {
261 var instanceIdString = instanceIdValues.FirstOrDefault();
262 if (instanceIdString != default && Int64.TryParse(instanceIdString, out var instanceId))
263 InstanceId = instanceId;
264 }
265
266 switch (scheme)
267 {
269 if (requestHeaders.Headers.TryGetValue(OAuthProviderHeader, out StringValues oauthProviderValues))
270 {
271 var oauthProviderString = oauthProviderValues.First();
272 if (Enum.TryParse<OAuthProvider>(oauthProviderString, true, out var oauthProvider))
273 OAuthProvider = oauthProvider;
274 else
275 AddError(HeaderErrorTypes.OAuthProvider, "Invalid OAuth provider!");
276 }
277 else
278 AddError(HeaderErrorTypes.OAuthProvider, $"Missing {OAuthProviderHeader} header!");
279
280 OAuthCode = parameter;
281 break;
283 Token = new TokenResponse
284 {
285 Bearer = parameter,
286 };
287
288 try
289 {
290 Token.ParseJwt();
291 }
292 catch (ArgumentException ex) when (ex is not ArgumentNullException)
293 {
294 AddError(HeaderErrorTypes.AuthorizationInvalid, $"Invalid JWT: {ex.Message}");
295 }
296
297 break;
299 string badBasicAuthHeaderMessage = $"Invalid basic {HeaderNames.Authorization} header!";
300 string joinedString;
301 try
302 {
303 var base64Bytes = Convert.FromBase64String(parameter);
304 joinedString = Encoding.UTF8.GetString(base64Bytes);
305 }
306 catch
307 {
308 AddError(HeaderErrorTypes.AuthorizationInvalid, badBasicAuthHeaderMessage);
309 break;
310 }
311
312 var basicAuthSplits = joinedString.Split(ColonSeparator, StringSplitOptions.RemoveEmptyEntries);
313 if (basicAuthSplits.Length < 2)
314 {
315 AddError(HeaderErrorTypes.AuthorizationInvalid, badBasicAuthHeaderMessage);
316 break;
317 }
318
319 Username = basicAuthSplits.First();
320 Password = String.Concat(basicAuthSplits.Skip(1));
321 break;
322 default:
323 AddError(HeaderErrorTypes.AuthorizationInvalid, "Invalid authentication scheme!");
324 break;
325 }
326 }
327 }
328 }
329
330 if (badHeaders != HeaderErrorTypes.None)
331 {
332 if (multipleErrors)
333 errorBuilder.Insert(0, $"Multiple header validation errors occurred:{Environment.NewLine}");
334
335 throw new HeadersException(badHeaders, errorBuilder.ToString());
336 }
337
338 ApiVersion = apiVersion!.Semver();
339 }
340#pragma warning restore CA1502
341
349 ApiHeaders(ProductHeaderValue userAgent, TokenResponse? token, string? username, string? password)
350 {
351 RawUserAgent = userAgent?.ToString();
352 Token = token;
353 Username = username;
354 Password = password;
356 }
357
363 public bool Compatible(Version? alternateApiVersion = null) => CheckCompatibility(ApiVersion) || (alternateApiVersion != null && alternateApiVersion.Major == ApiVersion.Major);
364
370 public void SetRequestHeaders(HttpRequestHeaders headers, long? instanceId = null)
371 {
372 if (headers == null)
373 throw new ArgumentNullException(nameof(headers));
374 if (instanceId.HasValue && InstanceId.HasValue && instanceId != InstanceId)
375 throw new InvalidOperationException("Specified different instance IDs in constructor and SetRequestHeaders!");
376
377 headers.Clear();
378 headers.Accept.Add(new MediaTypeWithQualityHeaderValue(ApplicationJsonMime));
379 headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent));
381 if (OAuthProvider.HasValue)
382 {
383 headers.Authorization = new AuthenticationHeaderValue(OAuthAuthenticationScheme, OAuthCode!);
384 headers.Add(OAuthProviderHeader, OAuthProvider.ToString());
385 }
386 else if (!IsTokenAuthentication)
387 headers.Authorization = new AuthenticationHeaderValue(
389 Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Username}:{Password}")));
390 else
391 headers.Authorization = new AuthenticationHeaderValue(BearerAuthenticationScheme, Token!.Bearer);
392
393 instanceId ??= InstanceId;
394 if (instanceId.HasValue)
395 headers.Add(InstanceIdHeader, instanceId.Value.ToString(CultureInfo.InvariantCulture));
396 }
397
402 public void SetHubConnectionHeaders(IDictionary<string, string> headers)
403 {
404 if (headers == null)
405 throw new ArgumentNullException(nameof(headers));
406
407 headers.Add(HeaderNames.UserAgent, RawUserAgent ?? throw new InvalidOperationException("Missing UserAgent!"));
408 headers.Add(HeaderNames.Accept, ApplicationJsonMime);
410 }
411
417 => new ProductHeaderValue(AssemblyName.Name, ApiVersion.ToString()).ToString();
418 }
419}
Represents the header that must be present for every server request.
Definition ApiHeaders.cs:25
ProductHeaderValue? UserAgent
The client's user agent as a ProductHeaderValue if valid.
Definition ApiHeaders.cs:89
ApiHeaders(ProductHeaderValue userAgent, TokenResponse token)
Initializes a new instance of the ApiHeaders class. Used for token authentication.
const string OAuthAuthenticationScheme
The JWT authentication header scheme.
Definition ApiHeaders.cs:54
string? RawUserAgent
The client's raw user agent.
Definition ApiHeaders.cs:94
bool Compatible(Version? alternateApiVersion=null)
Checks if the ApiVersion is compatible with Version.
const string InstanceIdHeader
The InstanceId header key.
Definition ApiHeaders.cs:34
void SetHubConnectionHeaders(IDictionary< string, string > headers)
Adds the headers necessary for a SignalR hub connection.
bool IsTokenAuthentication
If the header uses OAuth or TGS JWT authentication.
static bool CheckCompatibility(Version otherVersion)
Checks if a given otherVersion is compatible with our own.
string? Username
The client's username.
static readonly char[] ColonSeparator
A char Array containing the ':' char.
Definition ApiHeaders.cs:79
Version ApiVersion
The client's API version.
Definition ApiHeaders.cs:99
TokenResponse? Token
The client's TokenResponse.
void SetRequestHeaders(HttpRequestHeaders headers, long? instanceId=null)
Set HttpRequestHeaders using the ApiHeaders. This initially clears headers .
long? InstanceId
The instance EntityId.Id being accessed.
Definition ApiHeaders.cs:84
static readonly Version Version
Get the version of the Api the caller is using.
Definition ApiHeaders.cs:69
const string BasicAuthenticationScheme
The JWT authentication header scheme.
Definition ApiHeaders.cs:49
const string TextEventStreamMime
Added to MediaTypeNames.Application in netstandard2.1. Can't use because of lack of ....
Definition ApiHeaders.cs:64
string? OAuthCode
The OAuth code in use.
string CreateApiVersionHeader()
Create the stringified for of the ApiVersionHeader.
ApiHeaders(RequestHeaders requestHeaders, bool ignoreMissingAuth, bool allowEventStreamAccept)
Initializes a new instance of the ApiHeaders class.
const string ApiVersionHeader
The ApiVersion header key.
Definition ApiHeaders.cs:29
const string ApplicationJsonMime
Added to MediaTypeNames.Application in netstandard2.1. Can't use because of lack of ....
Definition ApiHeaders.cs:59
ApiHeaders(ProductHeaderValue userAgent, string oAuthCode, OAuthProvider oAuthProvider)
Initializes a new instance of the ApiHeaders class. Used for token authentication.
const string BearerAuthenticationScheme
The JWT authentication header scheme.
Definition ApiHeaders.cs:44
ApiHeaders(ProductHeaderValue userAgent, TokenResponse? token, string? username, string? password)
Initializes a new instance of the ApiHeaders class.
ApiHeaders(ProductHeaderValue userAgent, string username, string password)
Initializes a new instance of the ApiHeaders class. Used for password authentication.
static readonly AssemblyName AssemblyName
The current System.Reflection.AssemblyName.
Definition ApiHeaders.cs:74
const string OAuthProviderHeader
The OAuthProvider header key.
Definition ApiHeaders.cs:39
string? Password
The client's password.
Thrown when trying to generate ApiHeaders from Microsoft.AspNetCore.Http.Headers.RequestHeaders fails...
Represents a JWT returned by the API.
JsonWebToken ParseJwt()
Parses the Bearer as a JsonWebToken.
Attribute for bringing in the HTTP API version from MSBuild.
static ApiVersionAttribute Instance
Return the Assembly's instance of the ApiVersionAttribute.
string RawApiVersion
The Version string of the TGS API definition.
OAuthProvider
List of OAuth providers supported by TGS.
HeaderErrorTypes
Types of individual ApiHeaders errors.