tgstation-server  4.4.0
The /tg/station 13 server suite
ApiHeaders.cs
Go to the documentation of this file.
1 using Microsoft.AspNetCore.Http.Headers;
2 using Microsoft.Extensions.Primitives;
3 using Microsoft.Net.Http.Headers;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Net.Http.Headers;
8 using System.Net.Mime;
9 using System.Reflection;
10 using System.Text;
11 
12 namespace Tgstation.Server.Api
13 {
17  public sealed class ApiHeaders
18  {
22  public const string ApiVersionHeader = "Api";
23 
27  public const string InstanceIdHeader = "Instance";
28 
32  public const string JwtAuthenticationScheme = "bearer";
33 
37  public const string BasicAuthenticationScheme = "basic";
38 
42  static readonly AssemblyName AssemblyName = Assembly.GetExecutingAssembly().GetName();
43 
47  public static readonly Version Version = AssemblyName.Version.Semver();
48 
52  public long? InstanceId { get; set; }
53 
57  public ProductHeaderValue? UserAgent => ProductInfoHeaderValue.TryParse(RawUserAgent, out var userAgent) ? userAgent.Product : null;
58 
62  public string? RawUserAgent { get; }
63 
67  public Version ApiVersion { get; }
68 
72  public string? Token { get; }
73 
77  public string? Username { get; }
78 
82  public string? Password { get; }
83 
87  public bool IsTokenAuthentication => Token != null;
88 
94  public static bool CheckCompatibility(Version otherVersion) => Version.Major == (otherVersion?.Major ?? throw new ArgumentNullException(nameof(otherVersion)));
95 
101  public ApiHeaders(ProductHeaderValue userAgent, string token) : this(userAgent, token, null, null)
102  {
103  if (userAgent == null)
104  throw new ArgumentNullException(nameof(userAgent));
105  if (token == null)
106  throw new ArgumentNullException(nameof(token));
107  }
108 
115  public ApiHeaders(ProductHeaderValue userAgent, string username, string password) : this(userAgent, null, username, password)
116  {
117  if (userAgent == null)
118  throw new ArgumentNullException(nameof(userAgent));
119  if (username == null)
120  throw new ArgumentNullException(nameof(username));
121  if (password == null)
122  throw new ArgumentNullException(nameof(password));
123  }
124 
130  public ApiHeaders(RequestHeaders requestHeaders)
131  {
132  if (requestHeaders == null)
133  throw new ArgumentNullException(nameof(requestHeaders));
134 
135  var badHeaders = HeaderTypes.None;
136  var errorBuilder = new StringBuilder();
137 
138  void AddError(HeaderTypes headerType, string message)
139  {
140  if (badHeaders != HeaderTypes.None)
141  errorBuilder.Append(Environment.NewLine);
142  badHeaders |= headerType;
143  errorBuilder.Append(message);
144  }
145 
146  var jsonAccept = new Microsoft.Net.Http.Headers.MediaTypeHeaderValue(MediaTypeNames.Application.Json);
147  if (!requestHeaders.Accept.Any(x => jsonAccept.IsSubsetOf(x)))
148  AddError(HeaderTypes.Accept, $"Client does not accept {MediaTypeNames.Application.Json}!");
149 
150  if (!requestHeaders.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgentValues) || userAgentValues.Count == 0)
151  AddError(HeaderTypes.UserAgent, $"Missing {HeaderNames.UserAgent} header!");
152  else
153  {
154  RawUserAgent = userAgentValues.First();
155  if (String.IsNullOrWhiteSpace(RawUserAgent))
156  AddError(HeaderTypes.UserAgent, $"Malformed {HeaderNames.UserAgent} header!");
157  }
158 
159  // make sure the api header matches ours
160  Version? apiVersion = null;
161  if (!requestHeaders.Headers.TryGetValue(ApiVersionHeader, out var apiUserAgentHeaderValues) || !ProductInfoHeaderValue.TryParse(apiUserAgentHeaderValues.FirstOrDefault(), out var apiUserAgent) || apiUserAgent.Product.Name != AssemblyName.Name)
162  AddError(HeaderTypes.Api, $"Missing {ApiVersionHeader} header!");
163  else if (!Version.TryParse(apiUserAgent.Product.Version, out apiVersion))
164  AddError(HeaderTypes.Api, $"Malformed {ApiVersionHeader} header!");
165 
166  if (!requestHeaders.Headers.TryGetValue(HeaderNames.Authorization, out StringValues authorization))
167  AddError(HeaderTypes.Authorization, $"Missing {HeaderNames.Authorization} header!");
168  else
169  {
170  var auth = authorization.First();
171  var splits = new List<string>(auth.Split(' '));
172  var scheme = splits.First();
173  if (String.IsNullOrWhiteSpace(scheme))
174  AddError(HeaderTypes.Authorization, "Missing authentication scheme!");
175  else
176  {
177  splits.RemoveAt(0);
178  var parameter = String.Concat(splits);
179  if (String.IsNullOrEmpty(parameter))
180  AddError(HeaderTypes.Authorization, "Missing authentication parameter!");
181  else
182  {
183  if (requestHeaders.Headers.TryGetValue(InstanceIdHeader, out var instanceIdValues))
184  {
185  var instanceIdString = instanceIdValues.FirstOrDefault();
186  if (instanceIdString != default && Int64.TryParse(instanceIdString, out var instanceId))
187  InstanceId = instanceId;
188  }
189 
190 #pragma warning disable CA1308 // Normalize strings to uppercase
191  switch (scheme.ToLowerInvariant())
192 #pragma warning restore CA1308 // Normalize strings to uppercase
193  {
194  case JwtAuthenticationScheme:
195  Token = parameter;
196  break;
197  case BasicAuthenticationScheme:
198  string joinedString;
199  try
200  {
201  var base64Bytes = Convert.FromBase64String(parameter);
202  joinedString = Encoding.UTF8.GetString(base64Bytes);
203  }
204  catch
205  {
206  throw new InvalidOperationException("Invalid basic Authorization header!");
207  }
208 
209  var basicAuthSplits = joinedString.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
210  if (basicAuthSplits.Length < 2)
211  throw new InvalidOperationException("Invalid basic Authorization header!");
212 
213  Username = basicAuthSplits.First();
214  Password = String.Concat(basicAuthSplits.Skip(1));
215  break;
216  default:
217  AddError(HeaderTypes.Authorization, "Invalid authentication scheme!");
218  break;
219  }
220  }
221  }
222  }
223 
224  if (badHeaders != HeaderTypes.None)
225  throw new HeadersException(badHeaders, errorBuilder.ToString());
226 
227  ApiVersion = apiVersion!.Semver();
228  }
229 
237  ApiHeaders(ProductHeaderValue userAgent, string? token, string? username, string? password)
238  {
239  RawUserAgent = userAgent?.ToString();
240  Token = token;
241  Username = username;
242  Password = password;
243  ApiVersion = Version;
244  }
245 
250  public bool Compatible() => CheckCompatibility(ApiVersion);
251 
257  public void SetRequestHeaders(HttpRequestHeaders headers, long? instanceId = null)
258  {
259  if (headers == null)
260  throw new ArgumentNullException(nameof(headers));
261  if (instanceId.HasValue && InstanceId.HasValue && instanceId != InstanceId)
262  throw new InvalidOperationException("Specified different instance IDs in constructor and SetRequestHeaders!");
263 
264  headers.Clear();
265  headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
266  if (IsTokenAuthentication)
267  headers.Authorization = new AuthenticationHeaderValue(JwtAuthenticationScheme, Token);
268  else
269  headers.Authorization = new AuthenticationHeaderValue(
270  BasicAuthenticationScheme,
271  Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Username}:{Password}")));
272 
273  headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent));
274  headers.Add(ApiVersionHeader, new ProductHeaderValue(AssemblyName.Name, ApiVersion.ToString()).ToString());
275  instanceId ??= InstanceId;
276  if (instanceId.HasValue)
277  headers.Add(InstanceIdHeader, instanceId.ToString());
278  }
279  }
280 }
Thrown when trying to generate ApiHeaders from Microsoft.AspNetCore.Http.Headers.RequestHeaders fails...
ApiHeaders(ProductHeaderValue userAgent, string?token, string?username, string?password)
Construct ApiHeaders
Definition: ApiHeaders.cs:237
void SetRequestHeaders(HttpRequestHeaders headers, long?instanceId=null)
Set HttpRequestHeaders using the ApiHeaders. This initially clears headers
Definition: ApiHeaders.cs:257
HeaderTypes
Types of individual ApiHeaders.
Definition: HeaderTypes.cs:9
ApiHeaders(ProductHeaderValue userAgent, string username, string password)
Construct ApiHeaders for password authentication
Definition: ApiHeaders.cs:115
ApiHeaders(ProductHeaderValue userAgent, string token)
Construct ApiHeaders for JWT authentication
Definition: ApiHeaders.cs:101
Represents the header that must be present for every server request
Definition: ApiHeaders.cs:17
static readonly Version Version
Get the version of the Api the caller is using
Definition: ApiHeaders.cs:47
ApiHeaders(RequestHeaders requestHeaders)
Construct and validates ApiHeaders from a set of requestHeaders
Definition: ApiHeaders.cs:130
Microsoft.Net.Http.Headers.HeaderNames.UserAgent header.