tgstation-server 5.17.3
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 = new char[] { ':' };
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(jsonAccept.IsSubsetOf))
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, 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#pragma warning disable CS0618 // Type or member is obsolete
291 Token.ExpiresAt = Token.ParseJwt().ValidTo;
292#pragma warning restore CS0618 // Type or member is obsolete
293 }
294 catch (ArgumentException ex) when (ex is not ArgumentNullException)
295 {
296 AddError(HeaderErrorTypes.AuthorizationInvalid, $"Invalid JWT: {ex.Message}");
297 }
298
299 break;
301 string badBasicAuthHeaderMessage = $"Invalid basic {HeaderNames.Authorization} header!";
302 string joinedString;
303 try
304 {
305 var base64Bytes = Convert.FromBase64String(parameter);
306 joinedString = Encoding.UTF8.GetString(base64Bytes);
307 }
308 catch
309 {
310 AddError(HeaderErrorTypes.AuthorizationInvalid, badBasicAuthHeaderMessage);
311 break;
312 }
313
314 var basicAuthSplits = joinedString.Split(ColonSeparator, StringSplitOptions.RemoveEmptyEntries);
315 if (basicAuthSplits.Length < 2)
316 {
317 AddError(HeaderErrorTypes.AuthorizationInvalid, badBasicAuthHeaderMessage);
318 break;
319 }
320
321 Username = basicAuthSplits.First();
322 Password = String.Concat(basicAuthSplits.Skip(1));
323 break;
324 default:
325 AddError(HeaderErrorTypes.AuthorizationInvalid, "Invalid authentication scheme!");
326 break;
327 }
328 }
329 }
330 }
331
332 if (badHeaders != HeaderErrorTypes.None)
333 {
334 if (multipleErrors)
335 errorBuilder.Insert(0, $"Multiple header validation errors occurred:{Environment.NewLine}");
336
337 throw new HeadersException(badHeaders, errorBuilder.ToString());
338 }
339
340 ApiVersion = apiVersion!.Semver();
341 }
342#pragma warning restore CA1502
343
351 ApiHeaders(ProductHeaderValue userAgent, TokenResponse? token, string? username, string? password)
352 {
353 RawUserAgent = userAgent?.ToString();
354 Token = token;
355 Username = username;
356 Password = password;
358 }
359
365
371 public void SetRequestHeaders(HttpRequestHeaders headers, long? instanceId = null)
372 {
373 if (headers == null)
374 throw new ArgumentNullException(nameof(headers));
375 if (instanceId.HasValue && InstanceId.HasValue && instanceId != InstanceId)
376 throw new InvalidOperationException("Specified different instance IDs in constructor and SetRequestHeaders!");
377
378 headers.Clear();
379 headers.Accept.Add(new MediaTypeWithQualityHeaderValue(ApplicationJsonMime));
380 headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent));
382 if (OAuthProvider.HasValue)
383 {
384 headers.Authorization = new AuthenticationHeaderValue(OAuthAuthenticationScheme, OAuthCode!);
385 headers.Add(OAuthProviderHeader, OAuthProvider.ToString());
386 }
387 else if (!IsTokenAuthentication)
388 headers.Authorization = new AuthenticationHeaderValue(
390 Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Username}:{Password}")));
391 else
392 headers.Authorization = new AuthenticationHeaderValue(BearerAuthenticationScheme, Token!.Bearer);
393
394 instanceId ??= InstanceId;
395 if (instanceId.HasValue)
396 headers.Add(InstanceIdHeader, instanceId.Value.ToString(CultureInfo.InvariantCulture));
397 }
398
403 public void SetHubConnectionHeaders(IDictionary<string, string> headers)
404 {
405 if (headers == null)
406 throw new ArgumentNullException(nameof(headers));
407
408 headers.Add(HeaderNames.UserAgent, RawUserAgent ?? throw new InvalidOperationException("Missing UserAgent!"));
409 headers.Add(HeaderNames.Accept, ApplicationJsonMime);
411 }
412
418 => new ProductHeaderValue(AssemblyName.Name, ApiVersion.ToString()).ToString();
419 }
420}
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.
Definition: ApiHeaders.cs:143
const string OAuthAuthenticationScheme
The JWT authentication header scheme.
Definition: ApiHeaders.cs:54
string? RawUserAgent
The client's raw user agent.
Definition: ApiHeaders.cs:94
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.
Definition: ApiHeaders.cs:403
bool IsTokenAuthentication
If the header uses OAuth or TGS JWT authentication.
Definition: ApiHeaders.cs:129
bool Compatible()
Checks if the ApiVersion is compatible with Version.
static bool CheckCompatibility(Version otherVersion)
Checks if a given otherVersion is compatible with our own.
string? Username
The client's username.
Definition: ApiHeaders.cs:109
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.
Definition: ApiHeaders.cs:104
void SetRequestHeaders(HttpRequestHeaders headers, long? instanceId=null)
Set HttpRequestHeaders using the ApiHeaders. This initially clears headers .
Definition: ApiHeaders.cs:371
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.
Definition: ApiHeaders.cs:119
string CreateApiVersionHeader()
Create the stringified for of the ApiVersionHeader.
ApiHeaders(RequestHeaders requestHeaders, bool ignoreMissingAuth, bool allowEventStreamAccept)
Initializes a new instance of the ApiHeaders class.
Definition: ApiHeaders.cs:195
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.
Definition: ApiHeaders.cs:160
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.
Definition: ApiHeaders.cs:351
ApiHeaders(ProductHeaderValue userAgent, string username, string password)
Initializes a new instance of the ApiHeaders class. Used for password authentication.
Definition: ApiHeaders.cs:176
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.
Definition: ApiHeaders.cs:114
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.