/*
* Copyright (c) 2026 Proton AG
*
* This file is part of ProtonVPN.
*
* ProtonVPN is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonVPN is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonVPN. If not, see .
*/
using System;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.WebUtilities;
using Newtonsoft.Json;
using ProtonVPN.Api.Contracts;
using ProtonVPN.Api.Contracts.Common;
using ProtonVPN.Client.Settings.Contracts;
using ProtonVPN.Common.Core.Extensions;
using ProtonVPN.Common.Core.Geographical;
using ProtonVPN.Configurations.Contracts;
using ProtonVPN.Logging.Contracts;
using ProtonVPN.Logging.Contracts.Events.ApiLogs;
using TimeZoneConverter;
namespace ProtonVPN.Api;
public class BaseApiClient : IClientBase
{
protected ILogger Logger { get; }
protected ISettings Settings { get; }
protected IConfiguration Config { get; }
private readonly JsonSerializer _jsonSerializer = new();
private readonly IApiAppVersion _appVersion;
private readonly string _apiVersion;
public event EventHandler OnActionableFailureResult;
public BaseApiClient(
ILogger logger,
IApiAppVersion appVersion,
ISettings settings,
IConfiguration config)
{
Logger = logger;
Settings = settings;
Config = config;
_appVersion = appVersion;
_apiVersion = config.ApiVersion;
}
protected StringContent GetJsonContent(object data)
{
string json = JsonConvert.SerializeObject(data);
return new(json, Encoding.UTF8, "application/json");
}
protected async Task> GetApiResponseResultAsync(HttpResponseMessage response, CancellationToken cancellationToken = default)
{
if (typeof(T) == typeof(byte[]))
{
if (response.StatusCode is HttpStatusCode.NotModified)
{
return (ApiResponseResult)(object)ApiResponseResult.NotModified(response);
}
if (response.IsSuccessStatusCode)
{
byte[] body = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
return (ApiResponseResult)(object)ApiResponseResult.Ok(response, body);
}
else
{
return (ApiResponseResult)(object)ApiResponseResult.Fail(response, GetStatusCodeDescription(response.StatusCode));
}
}
if (typeof(T).IsAssignableTo(typeof(BaseResponse)))
{
string body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
try
{
T json = response.StatusCode is HttpStatusCode.NotModified
? default(T)
: JsonConvert.DeserializeObject(body) ?? throw new HttpRequestException(string.Empty);
ApiResponseResult result = CreateApiResponseResult(json, response);
HandleResult(result, response);
return result;
}
catch (JsonException ex)
{
throw new HttpRequestException(GetStatusCodeDescription(response.StatusCode), ex);
}
}
throw new NotSupportedException($"Type {typeof(T).Name} is not supported. Only BaseResponse types and byte[] are supported.");
}
public string GetStatusCodeDescription(HttpStatusCode code)
{
string description = ReasonPhrases.GetReasonPhrase((int)code);
return string.IsNullOrEmpty(description) ? $"HTTP error code: {code}." : description;
}
private ApiResponseResult CreateApiResponseResult(T response, HttpResponseMessage responseMessage)
{
if (responseMessage.StatusCode is HttpStatusCode.NotModified)
{
return ApiResponseResult.NotModified(responseMessage);
}
if (response is BaseResponse baseResponse)
{
return baseResponse.Code switch
{
ResponseCodes.OK_RESPONSE => ApiResponseResult.Ok(responseMessage, response),
_ => ApiResponseResult.Fail(response, responseMessage, baseResponse.Error),
};
}
return ApiResponseResult.Ok(responseMessage, response);
}
private void HandleResult(ApiResponseResult result, HttpResponseMessage responseMessage)
{
if (result.Failure && !result.Actions.IsNullOrEmpty())
{
HandleActionableFailureResult(result, responseMessage);
}
}
private void HandleActionableFailureResult(ApiResponseResult result, HttpResponseMessage responseMessage)
{
if (result.Value is BaseResponse baseResponse)
{
ApiResponseResult baseResponseResult = CreateApiResponseResult(baseResponse, responseMessage);
ActionableFailureApiResultEventArgs eventArgs = new(baseResponseResult);
OnActionableFailureResult?.Invoke(this, eventArgs);
}
}
protected HttpRequestMessage GetRequest(HttpMethod method, string requestUri)
{
string accessToken = string.IsNullOrEmpty(Settings.AccessToken)
? Settings.UnauthAccessToken
: Settings.AccessToken;
string uniqueSessionId = string.IsNullOrEmpty(Settings.UniqueSessionId)
? Settings.UnauthUniqueSessionId
: Settings.UniqueSessionId;
return !string.IsNullOrEmpty(accessToken) && !string.IsNullOrEmpty(uniqueSessionId)
? GetAuthorizedRequest(method, requestUri, accessToken, uniqueSessionId)
: GetUnauthorizedRequest(method, requestUri);
}
protected HttpRequestMessage GetAuthorizedRequest(HttpMethod method, string requestUri)
{
return GetAuthorizedRequest(method, requestUri, Settings.AccessToken, Settings.UniqueSessionId);
}
protected HttpRequestMessage GetAuthorizedRequest(HttpMethod method, string requestUri, string accessToken,
string uniqueSessionId)
{
HttpRequestMessage request = GetUnauthorizedRequest(method, requestUri);
request.Headers.Add("x-pm-uid", uniqueSessionId);
request.Headers.Add("Authorization", $"Bearer {accessToken}");
return request;
}
protected HttpRequestMessage GetUnauthorizedRequest(HttpMethod method, string requestUri)
{
HttpRequestMessage request = new(method, requestUri);
request.Headers.Add("x-pm-apiversion", _apiVersion);
request.Headers.Add("x-pm-appversion", _appVersion.AppVersion);
request.Headers.Add("x-pm-locale", Settings.Language);
request.Headers.Add("User-Agent", _appVersion.UserAgent);
try
{
request.Headers.Add("x-pm-timezone", TZConvert.WindowsToIana(TimeZoneInfo.Local.Id));
}
catch (Exception e)
{
Logger.Error("Failed to set x-pm-timezone header", e);
}
return request;
}
protected HttpRequestMessage GetAuthorizedRequestWithLocation(
HttpMethod method,
string requestUri,
DeviceLocation? deviceLocation)
{
HttpRequestMessage request = GetAuthorizedRequest(method, requestUri);
if (!string.IsNullOrEmpty(deviceLocation?.CountryCode))
{
request.Headers.Add("x-pm-country", deviceLocation.Value.CountryCode);
}
if (!string.IsNullOrEmpty(deviceLocation?.IpAddress))
{
request.Headers.Add("x-pm-netzone", deviceLocation.Value.IpAddress);
}
return request;
}
protected ApiResponseResult Logged(ApiResponseResult result, string message = null)
{
if (result.Failure)
{
Logger.Error($"API: {(string.IsNullOrEmpty(message) ? "Request" : message)} failed: {result.Error}");
}
return result;
}
}