using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Client.Extensions;
using Tgstation.Server.Common.Extensions;
using Tgstation.Server.Common.Http;
namespace Tgstation.Server.Client
{
///
class ApiClient : IApiClient
{
///
/// PATCH .
///
/// HOW IS THIS NOT INCLUDED IN THE FRAMEWORK??!?!?
static readonly HttpMethod HttpPatch = new("PATCH");
///
public Uri Url { get; }
///
public ApiHeaders Headers
{
get => headers;
set => headers = value ?? throw new InvalidOperationException("Cannot set null headers!");
}
///
public TimeSpan Timeout
{
get => httpClient.Timeout;
set => httpClient.Timeout = value;
}
///
/// The to use.
///
static readonly JsonSerializerSettings SerializerSettings = new()
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
Converters = new[]
{
new VersionConverter(),
},
};
///
/// The for the .
///
readonly HttpClient httpClient;
///
/// The s used by the .
///
readonly List requestLoggers;
///
/// List of s created by the .
///
readonly List hubConnections;
///
/// Backing field for .
///
readonly ApiHeaders? tokenRefreshHeaders;
///
/// The for refreshes.
///
readonly SemaphoreSlim semaphoreSlim;
///
/// If the authentication header should be stripped from requests.
///
readonly bool authless;
///
/// Backing field for .
///
ApiHeaders headers;
///
/// If the is disposed.
///
bool disposed;
///
/// Handle a bad HTTP .
///
/// The .
/// The JSON if any.
static void HandleBadResponse(HttpResponseMessage response, string json)
{
ErrorMessageResponse? errorMessage = null;
try
{
// check if json serializes to an error message
errorMessage = JsonConvert.DeserializeObject(json, SerializerSettings);
}
catch (JsonException)
{
}
#pragma warning disable IDE0010 // Add missing cases
switch (response.StatusCode)
#pragma warning restore IDE0010 // Add missing cases
{
case HttpStatusCode.Unauthorized:
throw new UnauthorizedException(errorMessage, response);
case HttpStatusCode.InternalServerError:
throw new ServerErrorException(errorMessage, response);
case HttpStatusCode.NotImplemented:
// unprocessable entity
case (HttpStatusCode)422:
throw new MethodNotSupportedException(errorMessage, response);
case HttpStatusCode.NotFound:
case HttpStatusCode.Gone:
case HttpStatusCode.Conflict:
throw new ConflictException(errorMessage, response);
case HttpStatusCode.Forbidden:
throw new InsufficientPermissionsException(response);
case HttpStatusCode.ServiceUnavailable:
throw new ServiceUnavailableException(response);
case HttpStatusCode.RequestTimeout:
throw new RequestTimeoutException(response);
case (HttpStatusCode)429:
throw new RateLimitException(errorMessage, response);
default:
if (errorMessage?.ErrorCode == ErrorCode.ApiMismatch)
throw new VersionMismatchException(errorMessage, response);
throw new ApiConflictException(errorMessage, response);
}
}
///
/// Initializes a new instance of the class.
///
/// The value of .
/// The value of .
/// The value of .
/// The value of .
/// The value of .
public ApiClient(
HttpClient httpClient,
Uri url,
ApiHeaders apiHeaders,
ApiHeaders? tokenRefreshHeaders,
bool authless)
{
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
Url = url ?? throw new ArgumentNullException(nameof(url));
headers = apiHeaders ?? throw new ArgumentNullException(nameof(apiHeaders));
this.tokenRefreshHeaders = tokenRefreshHeaders;
this.authless = authless;
requestLoggers = new List();
hubConnections = new List();
semaphoreSlim = new SemaphoreSlim(1);
}
///
public async ValueTask DisposeAsync()
{
List localHubConnections;
lock (hubConnections)
{
if (disposed)
return;
disposed = true;
localHubConnections = [.. hubConnections];
hubConnections.Clear();
}
await ValueTaskExtensions.WhenAll(hubConnections.Select(connection => connection.DisposeAsync()));
httpClient.Dispose();
semaphoreSlim.Dispose();
}
///
public ValueTask Create(string route, CancellationToken cancellationToken)
=> RunRequest