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 async ValueTask<HubConnection> AttemptConnect()
410 hubConnection = hubConnectionBuilder.Build();
413 hubConnection.Closed += async (error) =>
415 if (error is HttpRequestException httpRequestException)
418 var
property = error.GetType().GetProperty(
"StatusCode");
419 if (property !=
null)
421 var statusCode = (HttpStatusCode?)property.GetValue(error);
422 if (statusCode == HttpStatusCode.Unauthorized
424 _ = hubConnection!.StopAsync();
429 hubConnection.ProxyOn(hubImplementation);
435 throw new ObjectDisposedException(nameof(
ApiClient));
438 startTask = hubConnection.StartAsync(cancellationToken);
443 return hubConnection;
452 await hubConnection.DisposeAsync();
471#pragma warning disable CA1506
472 protected virtual async ValueTask<TResult> RunRequest<TResult>(
474 HttpContent? content,
478 CancellationToken cancellationToken)
481 throw new ArgumentNullException(nameof(route));
483 throw new ArgumentNullException(nameof(method));
484 if (content ==
null && (method == HttpMethod.Post || method == HttpMethod.Put))
485 throw new InvalidOperationException(
"content cannot be null for POST or PUT!");
488 throw new ObjectDisposedException(nameof(
ApiClient));
490 HttpResponseMessage response;
491 var fullUri =
new Uri(Url, route);
492 var serializerSettings = SerializerSettings;
493 var fileDownload = typeof(TResult) == typeof(
Stream);
494 using (var request =
new HttpRequestMessage(method, fullUri))
497 request.Content = content;
501 var headersToUse = tokenRefresh ? tokenRefreshHeaders! : headers;
505 request.Headers.Remove(HeaderNames.Authorization);
508 var bearer = headersToUse.Token?.Bearer;
511 var parsed = headersToUse.Token!.ParseJwt();
512 var nbf = parsed.ValidFrom;
513 var now = DateTime.UtcNow;
516 var delay = (nbf - now).Add(TimeSpan.FromMilliseconds(1));
517 await Task.Delay(delay, cancellationToken);
523 request.Headers.Accept.Add(
new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Octet));
525 await
ValueTaskExtensions.
WhenAll(requestLoggers.Select(x => x.LogRequest(request, cancellationToken))).ConfigureAwait(
false);
527 response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(
false);
532 request.Content =
null;
538 await
ValueTaskExtensions.
WhenAll(requestLoggers.Select(x => x.LogResponse(response, cancellationToken))).ConfigureAwait(
false);
541 if (fileDownload && response.IsSuccessStatusCode)
552 var json = await response.Content.ReadAsStringAsync().ConfigureAwait(
false);
554 if (!response.IsSuccessStatusCode)
557 && response.StatusCode == HttpStatusCode.Unauthorized
558 && await RefreshToken(cancellationToken).ConfigureAwait(
false))
559 return await RunRequest<TResult>(route, content, method, instanceId,
false, cancellationToken).ConfigureAwait(
false);
560 HandleBadResponse(response, json);
563 if (String.IsNullOrWhiteSpace(json))
564 json = JsonConvert.SerializeObject(
new object());
568 var result = JsonConvert.DeserializeObject<TResult>(json, serializerSettings);
571 catch (JsonException)
577#pragma warning restore CA1506
589 return await connectFunc();
591 catch (HttpRequestException ex)
594 var propertyInfo = ex.GetType().GetProperty(
"StatusCode");
595 if (propertyInfo !=
null)
597 var statusCode = (HttpStatusCode)propertyInfo.GetValue(ex);
598 if (statusCode != HttpStatusCode.Unauthorized)
602 await RefreshToken(cancellationToken);
604 return await connectFunc();
620 async ValueTask<TResult> RunRequest<TBody, TResult>(
626 CancellationToken cancellationToken)
629 HttpContent? content =
null;
631 content =
new StringContent(
632 JsonConvert.SerializeObject(body, typeof(TBody), Formatting.None, SerializerSettings),
637 return await RunRequest<TResult>(
644 .ConfigureAwait(
false);
658 async ValueTask RunResultlessRequest<TBody>(
664 CancellationToken cancellationToken)
666 => await RunRequest<TBody, object>(
688 CancellationToken cancellationToken)
689 => 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 IRestServerClient.An IAsyncDisposable representing the ...
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.
async ValueTask< HubConnection > WrapHubInitialConnectAuthRefresh(Func< ValueTask< HubConnection > > connectFunc, CancellationToken cancellationToken)
Wrap a hub connection attempt via a connectFunc with proper token refreshing.
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.