tgstation-server
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.Globalization;
7 using System.Linq;
8 using System.Net.Http.Headers;
9 using System.Reflection;
10 using System.Text;
11 
13 {
17  public sealed class ApiHeaders
18  {
22  public const string ApplicationJson = "application/json";
23 
27  public const string ApiVersionHeader = "api";
28 
32  public const string InstanceIdHeader = "instance";
33 
37  public const string JwtAuthenticationScheme = "bearer";
38 
42  public const string BasicAuthenticationScheme = "basic";
43 
47  const string UsernameHeader = "username";
48 
52  const string PasswordAuthenticationScheme = "password";
53 
57  static readonly AssemblyName AssemblyName = Assembly.GetExecutingAssembly().GetName();
58 
62  public static Version Version => AssemblyName.Version;
63 
67  public long? InstanceId { get; set; }
68 
72  public ProductHeaderValue UserAgent => ProductInfoHeaderValue.TryParse(RawUserAgent, out var userAgent) ? userAgent.Product : null;
73 
77  public string RawUserAgent { get; }
78 
82  public Version ApiVersion { get; }
83 
87  public string Token { get; }
88 
92  public string Username { get; }
93 
97  public string Password { get; }
98 
102  public bool IsTokenAuthentication => Token != null;
103 
109  public static bool CheckCompatibility(Version otherVersion) => !(Version.Major != (otherVersion?.Major ?? throw new ArgumentNullException(nameof(otherVersion))) || Version.Minor > otherVersion.Minor);
110 
116  public ApiHeaders(ProductHeaderValue userAgent, string token) : this(userAgent, token, null, null)
117  {
118  if (userAgent == null)
119  throw new ArgumentNullException(nameof(userAgent));
120  if (token == null)
121  throw new ArgumentNullException(nameof(token));
122  }
123 
130  public ApiHeaders(ProductHeaderValue userAgent, string username, string password) : this(userAgent, null, username, password)
131  {
132  if (userAgent == null)
133  throw new ArgumentNullException(nameof(userAgent));
134  if (username == null)
135  throw new ArgumentNullException(nameof(username));
136  if (password == null)
137  throw new ArgumentNullException(nameof(password));
138  }
139 
144  public ApiHeaders(RequestHeaders requestHeaders)
145  {
146  if (requestHeaders == null)
147  throw new ArgumentNullException(nameof(requestHeaders));
148 
149  var jsonAccept = new Microsoft.Net.Http.Headers.MediaTypeHeaderValue(ApplicationJson);
150  if (!requestHeaders.Accept.Any(x => x.MediaType == jsonAccept.MediaType))
151  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Client does not accept {0}!", ApplicationJson));
152 
153  if (!requestHeaders.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgentValues) || userAgentValues.Count == 0)
154  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Missing {0} headers!", HeaderNames.UserAgent));
155 
156  RawUserAgent = userAgentValues.First();
157  if (String.IsNullOrWhiteSpace(RawUserAgent))
158  throw new InvalidOperationException("Malformed client User-Agent!");
159 
160  // make sure the api header matches ours
161  if (!requestHeaders.Headers.TryGetValue(ApiVersionHeader, out var apiUserAgentHeaderValues) || !ProductInfoHeaderValue.TryParse(apiUserAgentHeaderValues.FirstOrDefault(), out var apiUserAgent) || apiUserAgent.Product.Name != AssemblyName.Name)
162  throw new InvalidOperationException("Missing API version!");
163 
164  if (!Version.TryParse(apiUserAgent.Product.Version, out var apiVersion))
165  throw new InvalidOperationException("Malformed API version!");
166 
167  ApiVersion = apiVersion;
168 
169  if (!requestHeaders.Headers.TryGetValue(HeaderNames.Authorization, out StringValues authorization))
170  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Missing {0} header!", HeaderNames.Authorization));
171  var auth = authorization.First();
172  var splits = new List<string>(auth.Split(' '));
173  var scheme = splits.First();
174  if (String.IsNullOrWhiteSpace(scheme))
175  throw new InvalidOperationException("Missing authentication scheme!");
176 
177  splits.RemoveAt(0);
178  var parameter = String.Concat(splits);
179  if (String.IsNullOrEmpty(parameter))
180  throw new InvalidOperationException("Missing authentication parameter!");
181 
182  if (requestHeaders.Headers.TryGetValue(InstanceIdHeader, out var instanceIdValues))
183  {
184  var instanceIdString = instanceIdValues.FirstOrDefault();
185  if (instanceIdString != default && Int64.TryParse(instanceIdString, out var instanceId))
186  InstanceId = instanceId;
187  }
188 
189 #pragma warning disable CA1308 // Normalize strings to uppercase
190  switch (scheme.ToLowerInvariant())
191 #pragma warning restore CA1308 // Normalize strings to uppercase
192  {
194  Token = parameter;
195  break;
197  Password = parameter;
198  var fail = !requestHeaders.Headers.TryGetValue(UsernameHeader, out var values);
199  if (!fail)
200  {
201  Username = values.FirstOrDefault();
202  fail = String.IsNullOrWhiteSpace(Username);
203  }
204 
205  if (fail)
206  throw new InvalidOperationException("Missing Username header!");
207  break;
209  string joinedString;
210  try
211  {
212  var base64Bytes = Convert.FromBase64String(parameter);
213  joinedString = Encoding.UTF8.GetString(base64Bytes);
214  }
215  catch
216  {
217  throw new InvalidOperationException("Invalid basic Authorization header!");
218  }
219 
220  var basicAuthSplits = joinedString.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
221  if (basicAuthSplits.Length < 2)
222  throw new InvalidOperationException("Invalid basic Authorization header!");
223 
224  Username = basicAuthSplits.First();
225  Password = String.Concat(basicAuthSplits.Skip(1));
226  break;
227  default:
228  throw new InvalidOperationException("Invalid authentication scheme!");
229  }
230  }
231 
239  ApiHeaders(ProductHeaderValue userAgent, string token, string username, string password)
240  {
241  RawUserAgent = userAgent?.ToString();
242  Token = token;
243  Username = username;
244  Password = password;
246  }
247 
252  public bool Compatible() => CheckCompatibility(ApiVersion);
253 
259  public void SetRequestHeaders(HttpRequestHeaders headers, long? instanceId = null)
260  {
261  if (headers == null)
262  throw new ArgumentNullException(nameof(headers));
263  if (instanceId.HasValue && InstanceId.HasValue && instanceId != InstanceId)
264  throw new InvalidOperationException("Specified instance ID in constructor and SetRequestHeaders!");
265 
266  headers.Clear();
267  headers.Accept.Add(new MediaTypeWithQualityHeaderValue(ApplicationJson));
269  headers.Authorization = new AuthenticationHeaderValue(JwtAuthenticationScheme, Token);
270  else
271  headers.Authorization = new AuthenticationHeaderValue(
273  Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Username}:{Password}")));
274 
275  headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent));
276  headers.Add(ApiVersionHeader, new ProductHeaderValue(AssemblyName.Name, ApiVersion.ToString()).ToString());
277  instanceId = instanceId ?? InstanceId;
278  if (instanceId.HasValue)
279  headers.Add(InstanceIdHeader, instanceId.ToString());
280  }
281  }
282 }
static readonly AssemblyName AssemblyName
The current System.Reflection.AssemblyName
Definition: ApiHeaders.cs:57
const string ApplicationJson
TODO: Remove this when https://github.com/dotnet/corefx/pull/26701 makes it into the sdk
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
const string PasswordAuthenticationScheme
The basic authentication header scheme
Definition: ApiHeaders.cs:52
string Username
The client's username
Definition: ApiHeaders.cs:92
ApiHeaders(ProductHeaderValue userAgent, string username, string password)
Construct ApiHeaders for password authentication
Definition: ApiHeaders.cs:130
ApiHeaders(ProductHeaderValue userAgent, string token)
Construct ApiHeaders for JWT authentication
Definition: ApiHeaders.cs:116
ProductHeaderValue UserAgent
The client's user agent as a ProductHeaderValue if valid
Definition: ApiHeaders.cs:72
Represents the header that must be present for every server request
Definition: ApiHeaders.cs:17
ApiHeaders(ProductHeaderValue userAgent, string token, string username, string password)
Construct ApiHeaders
Definition: ApiHeaders.cs:239
string Password
The client's password
Definition: ApiHeaders.cs:97
bool IsTokenAuthentication
If the header uses password or JWT authentication
Definition: ApiHeaders.cs:102
string RawUserAgent
The client's raw user agent
Definition: ApiHeaders.cs:77
ApiHeaders(RequestHeaders requestHeaders)
Construct and validates ApiHeaders from a set of requestHeaders
Definition: ApiHeaders.cs:144
void SetRequestHeaders(HttpRequestHeaders headers, long? instanceId=null)
Set HttpRequestHeaders using the ApiHeaders. This initially clears headers
Definition: ApiHeaders.cs:259
long? InstanceId
The Models.Instance.Id being accessed
Definition: ApiHeaders.cs:67
const string JwtAuthenticationScheme
The JWT authentication header scheme
Definition: ApiHeaders.cs:37
static bool CheckCompatibility(Version otherVersion)
Checks if a given otherVersion is compatible with our own
const string ApiVersionHeader
The ApiVersion header key
Definition: ApiHeaders.cs:27
const string InstanceIdHeader
The InstanceId header key
Definition: ApiHeaders.cs:32
string Token
The client's JWT
Definition: ApiHeaders.cs:87
const string UsernameHeader
The Username header key
Definition: ApiHeaders.cs:47
static Version Version
Get the version of the Api the caller is using
Definition: ApiHeaders.cs:62
const string BasicAuthenticationScheme
The JWT authentication header scheme
Definition: ApiHeaders.cs:42