tgstation-server 6.12.3
The /tg/station 13 server suite
Loading...
Searching...
No Matches
GraphQLServerClient.cs
Go to the documentation of this file.
1using System;
2using System.Diagnostics.CodeAnalysis;
3using System.Net.Http.Headers;
4using System.Threading;
5using System.Threading.Tasks;
6
7using Microsoft.Extensions.Logging;
8
9using StrawberryShake;
10
12
14{
17 {
21 [MemberNotNullWhen(true, nameof(setAuthenticationHeader))]
22 [MemberNotNullWhen(true, nameof(bearerCredentialsTask))]
24
28 [MemberNotNullWhen(true, nameof(bearerCredentialsHeaderTaskLock))]
29 [MemberNotNullWhen(true, nameof(basicCredentialsHeader))]
31
35 readonly IGraphQLClient graphQLClient;
36
41
45 readonly ILogger<GraphQLServerClient> logger;
46
50 readonly Action<AuthenticationHeaderValue>? setAuthenticationHeader;
51
55 readonly AuthenticationHeaderValue? basicCredentialsHeader;
56
61
65 Task<(AuthenticationHeaderValue Header, DateTime Exp)?>? bearerCredentialsTask;
66
71 [DoesNotReturn]
73 => throw new AuthenticationException("Another caller failed to authenticate!");
74
82 IGraphQLClient graphQLClient,
84 ILogger<GraphQLServerClient> logger)
85 {
86 this.graphQLClient = graphQLClient ?? throw new ArgumentNullException(nameof(graphQLClient));
87 this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
88 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
89 }
90
101 IGraphQLClient graphQLClient,
103 ILogger<GraphQLServerClient> logger,
104 Action<AuthenticationHeaderValue> setAuthenticationHeader,
105 AuthenticationHeaderValue? basicCredentialsHeader,
106 IOperationResult<ILoginResult> loginResult)
108 {
109 this.setAuthenticationHeader = setAuthenticationHeader ?? throw new ArgumentNullException(nameof(setAuthenticationHeader));
110 ArgumentNullException.ThrowIfNull(loginResult);
111 this.basicCredentialsHeader = basicCredentialsHeader;
112
113 var task = CreateCredentialsTuple(loginResult);
114 if (!task.IsCompleted)
115 throw new InvalidOperationException($"Expected {nameof(CreateCredentialsTuple)} to not await in constructor!");
116
117 bearerCredentialsTask = Task.FromResult<(AuthenticationHeaderValue Header, DateTime Exp)?>(task.Result);
118
119 if (Authenticated)
120 bearerCredentialsHeaderTaskLock = new object();
121 }
122
124 public virtual ValueTask DisposeAsync() => serviceProvider.DisposeAsync();
125
127 public ValueTask<IOperationResult<TResultData>> RunOperationAsync<TResultData>(Func<IGraphQLClient, ValueTask<IOperationResult<TResultData>>> operationExecutor, CancellationToken cancellationToken)
128 where TResultData : class
129 {
130 ArgumentNullException.ThrowIfNull(operationExecutor);
131 return WrapAuthentication(operationExecutor, cancellationToken);
132 }
133
135 public ValueTask<IOperationResult<TResultData>> RunOperation<TResultData>(Func<IGraphQLClient, Task<IOperationResult<TResultData>>> operationExecutor, CancellationToken cancellationToken)
136 where TResultData : class
137 {
138 ArgumentNullException.ThrowIfNull(operationExecutor);
139 return WrapAuthentication(async localClient => await operationExecutor(localClient), cancellationToken);
140 }
141
143 public async ValueTask<IDisposable> Subscribe<TResultData>(Func<IGraphQLClient, IObservable<IOperationResult<TResultData>>> operationExecutor, IObserver<IOperationResult<TResultData>> observer, CancellationToken cancellationToken)
144 where TResultData : class
145 {
146 ArgumentNullException.ThrowIfNull(operationExecutor);
147 ArgumentNullException.ThrowIfNull(observer);
148
149 var observable = operationExecutor(graphQLClient);
150
151 if (Authenticated)
152 {
153 var tuple = await bearerCredentialsTask.ConfigureAwait(false);
154 if (!tuple.HasValue)
156
157 var (currentAuthHeader, expires) = tuple.Value;
158 if (expires <= DateTimeOffset.UtcNow)
159 currentAuthHeader = await Reauthenticate(currentAuthHeader, cancellationToken).ConfigureAwait(false);
160
161 setAuthenticationHeader(currentAuthHeader);
162 }
163
164 // maybe make this handle reauthentication one day
165 // but would need to check if lost auth results in complete events being sent
166 // if so, it can't be done
167 return observable.Subscribe(observer);
168 }
169
175 protected virtual ValueTask<AuthenticationHeaderValue> CreateUpdatedAuthenticationHeader(string bearer)
176 => ValueTask.FromResult(
177 new AuthenticationHeaderValue(
179 bearer));
180
188 async ValueTask<IOperationResult<TResultData>> WrapAuthentication<TResultData>(Func<IGraphQLClient, ValueTask<IOperationResult<TResultData>>> operationExecutor, CancellationToken cancellationToken)
189 where TResultData : class
190 {
191 if (!Authenticated)
192 return await operationExecutor(graphQLClient).ConfigureAwait(false);
193
194 var tuple = await bearerCredentialsTask.ConfigureAwait(false);
195 if (!tuple.HasValue)
197
198 var (currentAuthHeader, expires) = tuple.Value;
199 if (expires <= DateTimeOffset.UtcNow)
200 currentAuthHeader = await Reauthenticate(currentAuthHeader, cancellationToken).ConfigureAwait(false);
201
202 setAuthenticationHeader(currentAuthHeader);
203
204 var operationResult = await operationExecutor(graphQLClient);
205
206 if (operationResult.IsAuthenticationError())
207 {
208 currentAuthHeader = await Reauthenticate(currentAuthHeader, cancellationToken).ConfigureAwait(false);
209 setAuthenticationHeader(currentAuthHeader);
210 return await operationExecutor(graphQLClient);
211 }
212
213 return operationResult;
214 }
215
222 async ValueTask<AuthenticationHeaderValue> Reauthenticate(AuthenticationHeaderValue currentToken, CancellationToken cancellationToken)
223 {
225 throw new AuthenticationException("Authentication expired or invalid and cannot re-authenticate.");
226
227 TaskCompletionSource<(AuthenticationHeaderValue Header, DateTime Exp)?>? tcs = null;
228 do
229 {
230 var bearerCredentialsTaskLocal = bearerCredentialsTask;
231 if (!bearerCredentialsTaskLocal!.IsCompleted)
232 {
233 var currentTuple = await bearerCredentialsTaskLocal.ConfigureAwait(false);
234 if (!currentTuple.HasValue)
236
237 return currentTuple.Value.Header;
238 }
239
241 {
242 if (bearerCredentialsTask == bearerCredentialsTaskLocal)
243 {
244 var result = bearerCredentialsTaskLocal.Result;
245 if (result?.Header != currentToken)
246 {
247 if (!result.HasValue)
249
250 return result.Value.Header;
251 }
252
253 tcs = new TaskCompletionSource<(AuthenticationHeaderValue, DateTime)?>();
254 bearerCredentialsTask = tcs.Task;
255 }
256 }
257 }
258 while (tcs == null);
259
261 var loginResult = await graphQLClient.Login.ExecuteAsync(cancellationToken).ConfigureAwait(false);
262 try
263 {
264 var tuple = await CreateCredentialsTuple(loginResult).ConfigureAwait(false);
265 tcs.SetResult(tuple);
266 return tuple.Header;
267 }
269 {
270 tcs.SetResult(null);
271 throw;
272 }
273 }
274
281 async ValueTask<(AuthenticationHeaderValue Header, DateTime Exp)> CreateCredentialsTuple(IOperationResult<ILoginResult> loginResult)
282 {
283 var bearer = loginResult.EnsureSuccess(logger);
284
285 var header = await CreateUpdatedAuthenticationHeader(bearer.EncodedToken);
286
287 return (Header: header, Exp: bearer.ValidTo);
288 }
289 }
290}
Represents the header that must be present for every server request.
Definition ApiHeaders.cs:25
const string BearerAuthenticationScheme
The JWT authentication header scheme.
Definition ApiHeaders.cs:44
Exception thrown when automatic IGraphQLServerClient authentication fails.
async ValueTask< AuthenticationHeaderValue > Reauthenticate(AuthenticationHeaderValue currentToken, CancellationToken cancellationToken)
Attempt to reauthenticate.
readonly IAsyncDisposable serviceProvider
The IAsyncDisposable to be DisposeAsync'd with the GraphQLServerClient.
GraphQLServerClient(IGraphQLClient graphQLClient, IAsyncDisposable serviceProvider, ILogger< GraphQLServerClient > logger, Action< AuthenticationHeaderValue > setAuthenticationHeader, AuthenticationHeaderValue? basicCredentialsHeader, IOperationResult< ILoginResult > loginResult)
Initializes a new instance of the GraphQLServerClient class.
Task<(AuthenticationHeaderValue Header, DateTime Exp)?>? bearerCredentialsTask
A Task<TResult> resulting in a ValueTuple<T1, T2> containing the current AuthenticationHeaderValue fo...
GraphQLServerClient(IGraphQLClient graphQLClient, IAsyncDisposable serviceProvider, ILogger< GraphQLServerClient > logger)
Initializes a new instance of the GraphQLServerClient class.
async ValueTask< IOperationResult< TResultData > > WrapAuthentication< TResultData >(Func< IGraphQLClient, ValueTask< IOperationResult< TResultData > > > operationExecutor, CancellationToken cancellationToken)
Executes a given operationExecutor , potentially accounting for authentication issues.
async ValueTask<(AuthenticationHeaderValue Header, DateTime Exp)> CreateCredentialsTuple(IOperationResult< ILoginResult > loginResult)
Attempt to create the ValueTuple<T1, T2> for bearerCredentialsTask.
readonly ILogger< GraphQLServerClient > logger
The ILogger for the GraphQLServerClient.
static void ThrowOtherCallerFailedAuthException()
Throws an AuthenticationException for a login error that previously occured outside of the current ca...
virtual ValueTask< AuthenticationHeaderValue > CreateUpdatedAuthenticationHeader(string bearer)
Create a AuthenticationHeaderValue from a given bearer token.
readonly IGraphQLClient graphQLClient
The IGraphQLClient for the GraphQLServerClient.
ValueTask< IOperationResult< TResultData > > RunOperation< TResultData >(Func< IGraphQLClient, Task< IOperationResult< TResultData > > > operationExecutor, CancellationToken cancellationToken)
Runs a given operationExecutor . It may be invoked multiple times depending on the behavior of the IG...
ValueTask< IOperationResult< TResultData > > RunOperationAsync< TResultData >(Func< IGraphQLClient, ValueTask< IOperationResult< TResultData > > > operationExecutor, CancellationToken cancellationToken)
Runs a given operationExecutor . It may be invoked multiple times depending on the behavior of the IG...
bool Authenticated
If the GraphQLServerClient was initially authenticated.
readonly? Action< AuthenticationHeaderValue > setAuthenticationHeader
The Action<T> which sets the AuthenticationHeaderValue for HTTP request in the current async context.
async ValueTask< IDisposable > Subscribe< TResultData >(Func< IGraphQLClient, IObservable< IOperationResult< TResultData > > > operationExecutor, IObserver< IOperationResult< TResultData > > observer, CancellationToken cancellationToken)
Subcribes to the GraphQL subscription indicated by operationExecutor .A ValueTask<TResult> resulting ...
readonly? object bearerCredentialsHeaderTaskLock
lock object used to synchronize access to bearerCredentialsTask.
bool CanReauthenticate
If the GraphQLServerClient supports reauthentication.
readonly? AuthenticationHeaderValue basicCredentialsHeader
The AuthenticationHeaderValue containing the authenticated user's password credentials.