2using System.Collections.Generic;
7using System.Net.Http.Headers;
10using System.Threading;
11using System.Threading.Tasks;
14using Microsoft.AspNetCore.Http.Connections;
15using Microsoft.AspNetCore.SignalR.Client;
16using Microsoft.Extensions.DependencyInjection;
17using Microsoft.Extensions.Logging;
18using Microsoft.Net.Http.Headers;
21using Newtonsoft.Json.Converters;
22using Newtonsoft.Json.Serialization;
40 static readonly HttpMethod
HttpPatch =
new(
"PATCH");
43 public Uri
Url {
get; }
49 set =>
headers = value ??
throw new InvalidOperationException(
"Cannot set null headers!");
56 set => httpClient.Timeout = value;
64 ContractResolver =
new CamelCasePropertyNamesContractResolver(),
67 new VersionConverter(),
124 catch (JsonException)
128#pragma warning disable IDE0010
129 switch (response.StatusCode)
130#pragma warning restore IDE0010
132 case HttpStatusCode.Unauthorized:
134 case HttpStatusCode.InternalServerError:
136 case HttpStatusCode.NotImplemented:
138 case (HttpStatusCode)422:
140 case HttpStatusCode.NotFound:
141 case HttpStatusCode.Gone:
142 case HttpStatusCode.Conflict:
144 case HttpStatusCode.Forbidden:
146 case HttpStatusCode.ServiceUnavailable:
148 case HttpStatusCode.RequestTimeout:
150 case (HttpStatusCode)429:
176 Url = url ??
throw new ArgumentNullException(nameof(url));
177 headers = apiHeaders ??
throw new ArgumentNullException(nameof(apiHeaders));
189 List<HubConnection> localHubConnections;
208 public ValueTask<TResult>
Create<TResult>(
string route, CancellationToken cancellationToken)
209 => RunRequest<object, TResult>(route,
new object(), HttpMethod.Put,
null,
false, cancellationToken);
212 public ValueTask<TResult>
Read<TResult>(
string route, CancellationToken cancellationToken)
213 => RunRequest<object, TResult>(route,
null, HttpMethod.Get,
null,
false, cancellationToken);
216 public ValueTask<TResult>
Update<TResult>(
string route, CancellationToken cancellationToken)
217 => RunRequest<object, TResult>(route,
new object(), HttpMethod.Post,
null,
false, cancellationToken);
225 public ValueTask
Patch(
string route, CancellationToken cancellationToken) =>
RunRequest(route,
HttpPatch,
null,
false, cancellationToken);
228 public ValueTask
Update<TBody>(
string route, TBody body, CancellationToken cancellationToken)
230 => RunResultlessRequest(route, body, HttpMethod.Post,
null,
false, cancellationToken);
238 public ValueTask
Delete(
string route, CancellationToken cancellationToken)
239 =>
RunRequest(route, HttpMethod.Delete,
null,
false, cancellationToken);
242 public ValueTask<TResult>
Create<TBody, TResult>(
string route, TBody body,
long instanceId, CancellationToken cancellationToken)
247 public ValueTask<TResult>
Read<TResult>(
string route,
long instanceId, CancellationToken cancellationToken)
251 public ValueTask<TResult>
Update<TBody, TResult>(
string route, TBody body,
long instanceId, CancellationToken cancellationToken)
256 public ValueTask
Delete(
string route,
long instanceId, CancellationToken cancellationToken)
257 =>
RunRequest(route, HttpMethod.Delete, instanceId,
false, cancellationToken);
260 public ValueTask
Delete<TBody>(
string route, TBody body,
long instanceId, CancellationToken cancellationToken)
262 => RunResultlessRequest(route, body, HttpMethod.Delete, instanceId,
false, cancellationToken);
265 public ValueTask<TResult>
Delete<TResult>(
string route,
long instanceId, CancellationToken cancellationToken)
266 =>
RunRequest<TResult>(route,
null, HttpMethod.Delete, instanceId,
false, cancellationToken);
269 public ValueTask<TResult>
Delete<TBody, TResult>(
string route, TBody body,
long instanceId, CancellationToken cancellationToken)
274 public ValueTask<TResult>
Create<TResult>(
string route,
long instanceId, CancellationToken cancellationToken)
275 => RunRequest<object, TResult>(route,
new object(), HttpMethod.Put, instanceId,
false, cancellationToken);
278 public ValueTask<TResult>
Patch<TResult>(
string route,
long instanceId, CancellationToken cancellationToken)
279 => RunRequest<object, TResult>(route,
new object(),
HttpPatch, instanceId,
false, cancellationToken);
288 throw new ArgumentNullException(nameof(ticket));
290 return RunRequest<Stream>(
291 $
"{Routes.Transfer}?ticket={HttpUtility.UrlEncode(ticket.FileTicket)}",
303 throw new ArgumentNullException(nameof(ticket));
305 MemoryStream? memoryStream =
null;
306 if (uploadStream ==
null)
307 memoryStream =
new MemoryStream();
311 var streamContent =
new StreamContent(uploadStream ?? memoryStream);
314 await RunRequest<object>(
315 $
"{Routes.Transfer}?ticket={HttpUtility.UrlEncode(ticket.FileTicket)}",
321 .ConfigureAwait(
false);
322 streamContent =
null;
326 streamContent?.Dispose();
336 public async ValueTask<bool>
RefreshToken(CancellationToken cancellationToken)
342 await
semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(
false);
348 var token = await RunRequest<object, TokenResponse>(
Routes.
ApiRoot,
new object(), HttpMethod.Post,
null,
true, cancellationToken).ConfigureAwait(
false);
361 THubImplementation hubImplementation,
363 Action<ILoggingBuilder>? loggingConfigureAction,
364 CancellationToken cancellationToken)
365 where THubImplementation :
class
367 if (hubImplementation ==
null)
368 throw new ArgumentNullException(nameof(hubImplementation));
374 HubConnection? hubConnection =
null;
375 var hubConnectionBuilder =
new HubConnectionBuilder()
376 .AddNewtonsoftJsonProtocol(options =>
380 .WithAutomaticReconnect(wrappedPolicy)
383 HttpTransportType.ServerSentEvents,
386 options.AccessTokenProvider = async () =>
389 if (Headers.Token == null
390 || (Headers.Token.ParseJwt().ValidTo <= DateTime.UtcNow
391 && !await RefreshToken(CancellationToken.None)))
393 _ = hubConnection!.StopAsync();
397 return Headers.Token.Bearer;
400 options.CloseTimeout =
Timeout;
405 if (loggingConfigureAction !=
null)
406 hubConnectionBuilder.ConfigureLogging(loggingConfigureAction);
408 hubConnection = hubConnectionBuilder.Build();
411 hubConnection.Closed += async (error) =>
413 if (error is HttpRequestException httpRequestException)
416 var
property = error.GetType().GetProperty(
"StatusCode");
417 if (property !=
null)
419 var statusCode = (HttpStatusCode?)property.GetValue(error);
420 if (statusCode == HttpStatusCode.Unauthorized
422 _ = hubConnection!.StopAsync();
427 hubConnection.ProxyOn(hubImplementation);
433 throw new ObjectDisposedException(nameof(
ApiClient));
436 startTask = hubConnection.StartAsync(cancellationToken);
441 return hubConnection;
450 await hubConnection.DisposeAsync();
466#pragma warning disable CA1506
467 protected virtual async ValueTask<TResult> RunRequest<TResult>(
469 HttpContent? content,
473 CancellationToken cancellationToken)
476 throw new ArgumentNullException(nameof(route));
478 throw new ArgumentNullException(nameof(method));
479 if (content ==
null && (method == HttpMethod.Post || method == HttpMethod.Put))
480 throw new InvalidOperationException(
"content cannot be null for POST or PUT!");
483 throw new ObjectDisposedException(nameof(
ApiClient));
485 HttpResponseMessage response;
486 var fullUri =
new Uri(Url, route);
487 var serializerSettings = SerializerSettings;
488 var fileDownload = typeof(TResult) == typeof(
Stream);
489 using (var request =
new HttpRequestMessage(method, fullUri))
492 request.Content = content;
496 var headersToUse = tokenRefresh ? tokenRefreshHeaders! : headers;
500 request.Headers.Remove(HeaderNames.Authorization);
503 var bearer = headersToUse.Token?.Bearer;
506 var parsed = headersToUse.Token!.ParseJwt();
507 var nbf = parsed.ValidFrom;
508 var now = DateTime.UtcNow;
511 var delay = (nbf - now).Add(TimeSpan.FromMilliseconds(1));
512 await Task.Delay(delay, cancellationToken);
518 request.Headers.Accept.Add(
new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Octet));
520 await
ValueTaskExtensions.
WhenAll(requestLoggers.Select(x => x.LogRequest(request, cancellationToken))).ConfigureAwait(
false);
522 response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(
false);
527 request.Content =
null;
533 await
ValueTaskExtensions.
WhenAll(requestLoggers.Select(x => x.LogResponse(response, cancellationToken))).ConfigureAwait(
false);
536 if (fileDownload && response.IsSuccessStatusCode)
547 var json = await response.Content.ReadAsStringAsync().ConfigureAwait(
false);
549 if (!response.IsSuccessStatusCode)
552 && response.StatusCode == HttpStatusCode.Unauthorized
553 && await RefreshToken(cancellationToken).ConfigureAwait(
false))
554 return await RunRequest<TResult>(route, content, method, instanceId,
false, cancellationToken).ConfigureAwait(
false);
555 HandleBadResponse(response, json);
558 if (String.IsNullOrWhiteSpace(json))
559 json = JsonConvert.SerializeObject(
new object());
563 var result = JsonConvert.DeserializeObject<TResult>(json, serializerSettings);
566 catch (JsonException)
572#pragma warning restore CA1506
586 async ValueTask<TResult> RunRequest<TBody, TResult>(
592 CancellationToken cancellationToken)
595 HttpContent? content =
null;
597 content =
new StringContent(
598 JsonConvert.SerializeObject(body, typeof(TBody), Formatting.None, SerializerSettings),
603 return await RunRequest<TResult>(
610 .ConfigureAwait(
false);
624 async ValueTask RunResultlessRequest<TBody>(
630 CancellationToken cancellationToken)
632 => await RunRequest<TBody, object>(
654 CancellationToken cancellationToken)
655 => RunResultlessRequest<object>(
Represents an error message returned by the server.
Response for when file transfers are necessary.
Routes to a server actions.
const string ApiRoot
The root of API methods.
const string JobsHub
The root route of all hubs.
readonly? ApiHeaders tokenRefreshHeaders
Backing field for Headers.
TimeSpan Timeout
The request timeout.
static readonly JsonSerializerSettings SerializerSettings
The JsonSerializerSettings to use.
ValueTask< Stream > Download(FileTicketResponse ticket, CancellationToken cancellationToken)
Downloads a file Stream for a given ticket .A ValueTask<TResult> resulting in the downloaded Stream.
void AddRequestLogger(IRequestLogger requestLogger)
Adds a requestLogger to the request pipeline.
async ValueTask< TResult > RunRequest< TBody, TResult >(string route, TBody? body, HttpMethod method, long? instanceId, bool tokenRefresh, CancellationToken cancellationToken)
Main request method.
readonly IHttpClient httpClient
The IHttpClient for the ApiClient.
async ValueTask Upload(FileTicketResponse ticket, Stream? uploadStream, CancellationToken cancellationToken)
Uploads a given uploadStream for a given ticket .A ValueTask representing the running operation.
ValueTask< TResult > Delete< TResult >(string route, long instanceId, CancellationToken cancellationToken)
Run an HTTP DELETE request.A ValueTask<TResult> resulting in the response body as a TResult .
Uri Url
The Uri pointing the tgstation-server.
ValueTask Delete(string route, CancellationToken cancellationToken)
Run an HTTP DELETE request.A ValueTask representing the running operation.
ValueTask< TResult > Create< TBody, TResult >(string route, TBody body, CancellationToken cancellationToken)
Run an HTTP PUT request.A ValueTask<TResult> resulting in the response body as a TResult .
ApiHeaders headers
Backing field for Headers.
ValueTask< TResult > Update< TBody, TResult >(string route, TBody body, CancellationToken cancellationToken)
Run an HTTP POST request.A ValueTask<TResult> resulting in the response body as a TResult .
ValueTask Delete< TBody >(string route, TBody body, long instanceId, CancellationToken cancellationToken)
Run an HTTP DELETE request.A ValueTask representing the running operation.
ValueTask< TResult > Read< TResult >(string route, CancellationToken cancellationToken)
Run an HTTP GET request.A ValueTask<TResult> resulting in the response body as a TResult .
ValueTask Patch(string route, CancellationToken cancellationToken)
Run an HTTP PATCH request.A ValueTask representing the running operation.
static void HandleBadResponse(HttpResponseMessage response, string json)
Handle a bad HTTP response .
static readonly HttpMethod HttpPatch
PATCH HttpMethod.
ValueTask< TResult > Delete< TBody, TResult >(string route, TBody body, long instanceId, CancellationToken cancellationToken)
Run an HTTP DELETE request.A ValueTask representing the running operation.
virtual async ValueTask< TResult > RunRequest< TResult >(string route, HttpContent? content, HttpMethod method, long? instanceId, bool tokenRefresh, CancellationToken cancellationToken)
Main request method.
readonly List< IRequestLogger > requestLoggers
The IRequestLoggers used by the ApiClient.
async ValueTask< IAsyncDisposable > CreateHubConnection< THubImplementation >(THubImplementation hubImplementation, IRetryPolicy? retryPolicy, Action< ILoggingBuilder >? loggingConfigureAction, CancellationToken cancellationToken)
Subscribe to all job updates available to the IServerClient.An IAsyncDisposable representing the life...
ValueTask< TResult > Create< TResult >(string route, CancellationToken cancellationToken)
Run an HTTP PUT request.A ValueTask<TResult> resulting in the response body as a TResult .
ValueTask< TResult > Update< TResult >(string route, CancellationToken cancellationToken)
Run an HTTP POST request.A ValueTask<TResult> resulting in the response body as a TResult .
ValueTask< TResult > Patch< TResult >(string route, long instanceId, CancellationToken cancellationToken)
Run an HTTP PATCH request.A ValueTask<TResult> resulting in the response body as a TResult .
async ValueTask DisposeAsync()
bool disposed
If the ApiClient is disposed.
readonly bool authless
If the authentication header should be stripped from requests.
readonly SemaphoreSlim semaphoreSlim
The SemaphoreSlim for TokenResponse refreshes.
ValueTask RunRequest(string route, HttpMethod method, long? instanceId, bool tokenRefresh, CancellationToken cancellationToken)
Main request method.
ValueTask Delete(string route, long instanceId, CancellationToken cancellationToken)
Run an HTTP DELETE request.A ValueTask representing the running operation.
readonly List< HubConnection > hubConnections
List of HubConnections created by the ApiClient.
async ValueTask< bool > RefreshToken(CancellationToken cancellationToken)
Attempt to refresh the stored Bearer token in Headers.
ApiHeaders Headers
The ApiHeaders the IApiClient uses.
ValueTask Update< TBody >(string route, TBody body, CancellationToken cancellationToken)
Run an HTTP POST request.A ValueTask representing the running operation.
ApiClient(IHttpClient httpClient, Uri url, ApiHeaders apiHeaders, ApiHeaders? tokenRefreshHeaders, bool authless)
Initializes a new instance of the ApiClient class.
A IRetryPolicy that attempts to refresh a given apiClient's token on the first disconnect.
Occurs when the server returns a bad request response if the ApiException.ErrorCode is present....
Occurs when the client performs an action that would result in data conflict.
A IRetryPolicy that returns seconds in powers of 2, maxing out at 30s.
Occurs when the client attempts to perform an action they do not have the rights for.
Occurs when the client tries to use a currently unsupported API.
Occurs when a GitHub rate limit occurs.
Occurs when the client provides invalid credentials.
Occurs when an error occurs in the server.
Occurs when the client makes a request while the server is starting or stopping.
Occurs when the client provides invalid credentials.
Occurs when a response is received that did not deserialize to one of the expected Api....
Occurs when the API version of the client is not compatible with the server's.
Extension methods for the ValueTask and ValueTask<TResult> classes.
static async ValueTask WhenAll(IEnumerable< ValueTask > tasks)
Fully await a given list of tasks .
Caches the Stream from a HttpResponseMessage for later use.
static async ValueTask< CachedResponseStream > Create(HttpResponseMessage response)
Asyncronously creates a new CachedResponseStream.
Web interface for the API.
For logging HTTP requests and responses.
For sending HTTP requests.
TimeSpan Timeout
The request timeout.
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.