using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.Models.Request; using Bit.Core.Models.Response; using Bit.Core.Utilities; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; using DeviceType = Bit.Core.Enums.DeviceType; namespace Bit.Core.Services { public class ApiService : IApiService { private readonly JsonSerializerSettings _jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }; private readonly HttpClient _httpClient = new HttpClient(); private readonly ITokenService _tokenService; private readonly IPlatformUtilsService _platformUtilsService; private readonly Func, Task> _logoutCallbackAsync; public ApiService( ITokenService tokenService, IPlatformUtilsService platformUtilsService, Func, Task> logoutCallbackAsync, string customUserAgent = null) { _tokenService = tokenService; _platformUtilsService = platformUtilsService; _logoutCallbackAsync = logoutCallbackAsync; var device = (int)_platformUtilsService.GetDevice(); _httpClient.DefaultRequestHeaders.Add("Device-Type", device.ToString()); _httpClient.DefaultRequestHeaders.Add("Bitwarden-Client-Name", _platformUtilsService.GetClientType().GetString()); _httpClient.DefaultRequestHeaders.Add("Bitwarden-Client-Version", VersionHelpers.RemoveSuffix(_platformUtilsService.GetApplicationVersion())); if (!string.IsNullOrWhiteSpace(customUserAgent)) { _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(customUserAgent); } } public bool UrlsSet { get; private set; } public string ApiBaseUrl { get; set; } public string IdentityBaseUrl { get; set; } public string EventsBaseUrl { get; set; } public void SetUrls(EnvironmentUrlData urls) { UrlsSet = true; if (!string.IsNullOrWhiteSpace(urls.Base)) { ApiBaseUrl = urls.Base + "/api"; IdentityBaseUrl = urls.Base + "/identity"; EventsBaseUrl = urls.Base + "/events"; return; } ApiBaseUrl = urls.Api; IdentityBaseUrl = urls.Identity; EventsBaseUrl = urls.Events; // Production if (string.IsNullOrWhiteSpace(ApiBaseUrl)) { ApiBaseUrl = "https://api.bitwarden.com"; } if (string.IsNullOrWhiteSpace(IdentityBaseUrl)) { IdentityBaseUrl = "https://identity.bitwarden.com"; } if (string.IsNullOrWhiteSpace(EventsBaseUrl)) { EventsBaseUrl = "https://events.bitwarden.com"; } } #region Auth APIs public async Task PostIdentityTokenAsync(TokenRequest request) { var requestMessage = new HttpRequestMessage { Version = new Version(1, 0), RequestUri = new Uri(string.Concat(IdentityBaseUrl, "/connect/token")), Method = HttpMethod.Post, Content = new FormUrlEncodedContent(request.ToIdentityToken(_platformUtilsService.GetClientType().GetString())) }; requestMessage.Headers.Add("Accept", "application/json"); request.AlterIdentityTokenHeaders(requestMessage.Headers); HttpResponseMessage response; try { response = await _httpClient.SendAsync(requestMessage); } catch (Exception e) { throw new ApiException(HandleWebError(e)); } JObject responseJObject = null; if (IsJsonResponse(response)) { var responseJsonString = await response.Content.ReadAsStringAsync(); responseJObject = JObject.Parse(responseJsonString); } var identityResponse = new IdentityResponse(response.StatusCode, responseJObject); if (identityResponse.FailedToParse) { throw new ApiException(new ErrorResponse(responseJObject, response.StatusCode, true)); } return identityResponse; } public async Task RefreshIdentityTokenAsync() { try { await DoRefreshTokenAsync(); } catch { throw new ApiException(); } } #endregion #region Account APIs public Task GetProfileAsync() { return SendAsync(HttpMethod.Get, "/accounts/profile", null, true, true); } public Task PostPreloginAsync(PreloginRequest request) { return SendAsync(HttpMethod.Post, "/accounts/prelogin", request, false, true, sendToIdentity: true); } public Task GetAccountRevisionDateAsync() { return SendAsync(HttpMethod.Get, "/accounts/revision-date", null, true, true); } public Task PostPasswordHintAsync(PasswordHintRequest request) { return SendAsync(HttpMethod.Post, "/accounts/password-hint", request, false, false); } public Task SetPasswordAsync(SetPasswordRequest request) { return SendAsync(HttpMethod.Post, "/accounts/set-password", request, true, false); } public Task PostRegisterAsync(RegisterRequest request) { return SendAsync(HttpMethod.Post, "/accounts/register", request, false, false, sendToIdentity: true); } public Task PostAccountKeysAsync(KeysRequest request) { return SendAsync(HttpMethod.Post, "/accounts/keys", request, true, false); } public Task PostAccountVerifyPasswordAsync(PasswordVerificationRequest request) { return SendAsync(HttpMethod.Post, "/accounts/verify-password", request, true, true); } public Task PostAccountRequestOTP() { return SendAsync(HttpMethod.Post, "/accounts/request-otp", null, true, false, null, false); } public Task PostAccountVerifyOTPAsync(VerifyOTPRequest request) { return SendAsync(HttpMethod.Post, "/accounts/verify-otp", request, true, false, null, false); } public Task PutUpdateTempPasswordAsync(UpdateTempPasswordRequest request) { return SendAsync(HttpMethod.Put, "/accounts/update-temp-password", request, true, false); } public Task PostPasswordAsync(PasswordRequest request) { return SendAsync(HttpMethod.Post, "/accounts/password", request, true, false); } public Task DeleteAccountAsync(DeleteAccountRequest request) { return SendAsync(HttpMethod.Delete, "/accounts", request, true, false); } public Task PostConvertToKeyConnectorAsync() { return SendAsync(HttpMethod.Post, "/accounts/convert-to-key-connector", null, true, false); } public Task PostSetKeyConnectorKeyAsync(SetKeyConnectorKeyRequest request) { return SendAsync(HttpMethod.Post, "/accounts/set-key-connector-key", request, true); } #endregion #region Folder APIs public Task GetFolderAsync(string id) { return SendAsync(HttpMethod.Get, string.Concat("/folders/", id), null, true, true); } public Task PostFolderAsync(FolderRequest request) { return SendAsync(HttpMethod.Post, "/folders", request, true, true); } public async Task PutFolderAsync(string id, FolderRequest request) { return await SendAsync(HttpMethod.Put, string.Concat("/folders/", id), request, true, true); } public Task DeleteFolderAsync(string id) { return SendAsync(HttpMethod.Delete, string.Concat("/folders/", id), null, true, false); } #endregion #region Send APIs public Task GetSendAsync(string id) => SendAsync(HttpMethod.Get, $"/sends/{id}", null, true, true); public Task PostSendAsync(SendRequest request) => SendAsync(HttpMethod.Post, "/sends", request, true, true); public Task PostFileTypeSendAsync(SendRequest request) => SendAsync(HttpMethod.Post, "/sends/file/v2", request, true, true); public Task PostSendFileAsync(string sendId, string fileId, MultipartFormDataContent data) => SendAsync(HttpMethod.Post, $"/sends/{sendId}/file/{fileId}", data, true, false); [Obsolete("Mar 25 2021: This method has been deprecated in favor of direct uploads. This method still exists for backward compatibility with old server versions.")] public Task PostSendFileAsync(MultipartFormDataContent data) => SendAsync(HttpMethod.Post, "/sends/file", data, true, true); public Task RenewFileUploadUrlAsync(string sendId, string fileId) => SendAsync(HttpMethod.Get, $"/sends/{sendId}/file/{fileId}", null, true, true); public Task PutSendAsync(string id, SendRequest request) => SendAsync(HttpMethod.Put, $"/sends/{id}", request, true, true); public Task PutSendRemovePasswordAsync(string id) => SendAsync(HttpMethod.Put, $"/sends/{id}/remove-password", null, true, true); public Task DeleteSendAsync(string id) => SendAsync(HttpMethod.Delete, $"/sends/{id}", null, true, false); #endregion #region Cipher APIs public Task GetCipherAsync(string id) { return SendAsync(HttpMethod.Get, string.Concat("/ciphers/", id), null, true, true); } public Task PostCipherAsync(CipherRequest request) { return SendAsync(HttpMethod.Post, "/ciphers", request, true, true); } public Task PostCipherCreateAsync(CipherCreateRequest request) { return SendAsync(HttpMethod.Post, "/ciphers/create", request, true, true); } public Task PutCipherAsync(string id, CipherRequest request) { return SendAsync(HttpMethod.Put, string.Concat("/ciphers/", id), request, true, true); } public Task PutShareCipherAsync(string id, CipherShareRequest request) { return SendAsync(HttpMethod.Put, string.Concat("/ciphers/", id, "/share"), request, true, true); } public Task PutCipherCollectionsAsync(string id, CipherCollectionsRequest request) { return SendAsync(HttpMethod.Put, string.Concat("/ciphers/", id, "/collections"), request, true, false); } public Task DeleteCipherAsync(string id) { return SendAsync(HttpMethod.Delete, string.Concat("/ciphers/", id), null, true, false); } public Task PutDeleteCipherAsync(string id) { return SendAsync(HttpMethod.Put, string.Concat("/ciphers/", id, "/delete"), null, true, false); } public Task PutRestoreCipherAsync(string id) { return SendAsync(HttpMethod.Put, string.Concat("/ciphers/", id, "/restore"), null, true, true); } public Task HasUnassignedCiphersAsync() { return SendAsync(HttpMethod.Get, "/ciphers/has-unassigned-ciphers", null, true, true); } #endregion #region Attachments APIs [Obsolete("Mar 25 2021: This method has been deprecated in favor of direct uploads. This method still exists for backward compatibility with old server versions.")] public Task PostCipherAttachmentLegacyAsync(string id, MultipartFormDataContent data) { return SendAsync(HttpMethod.Post, string.Concat("/ciphers/", id, "/attachment"), data, true, true); } public Task PostCipherAttachmentAsync(string id, AttachmentRequest request) { return SendAsync(HttpMethod.Post, $"/ciphers/{id}/attachment/v2", request, true, true); } public Task GetAttachmentData(string cipherId, string attachmentId) => SendAsync(HttpMethod.Get, $"/ciphers/{cipherId}/attachment/{attachmentId}", true); public Task DeleteCipherAttachmentAsync(string id, string attachmentId) { return SendAsync(HttpMethod.Delete, string.Concat("/ciphers/", id, "/attachment/", attachmentId), true); } public Task PostShareCipherAttachmentAsync(string id, string attachmentId, MultipartFormDataContent data, string organizationId) { return SendAsync(HttpMethod.Post, string.Concat("/ciphers/", id, "/attachment/", attachmentId, "/share?organizationId=", organizationId), data, true, false); } public Task RenewAttachmentUploadUrlAsync(string cipherId, string attachmentId) => SendAsync(HttpMethod.Get, $"/ciphers/{cipherId}/attachment/{attachmentId}/renew", true); public Task PostAttachmentFileAsync(string cipherId, string attachmentId, MultipartFormDataContent data) => SendAsync(HttpMethod.Post, $"/ciphers/{cipherId}/attachment/{attachmentId}", data, true); #endregion #region Sync APIs public Task GetSyncAsync() { return SendAsync(HttpMethod.Get, "/sync", null, true, true); } #endregion #region Two Factor APIs public Task PostTwoFactorEmailAsync(TwoFactorEmailRequest request) { return SendAsync( HttpMethod.Post, "/two-factor/send-email-login", request, false, false); } #endregion #region Device APIs public Task GetKnownDeviceAsync(string email, string deviceIdentifier) { return SendAsync(HttpMethod.Get, "/devices/knowndevice", null, false, true, (message) => { message.Headers.Add("X-Device-Identifier", deviceIdentifier); message.Headers.Add("X-Request-Email", CoreHelpers.Base64UrlEncode(Encoding.UTF8.GetBytes(email))); }); } public Task PutDeviceTokenAsync(string identifier, DeviceTokenRequest request) { return SendAsync( HttpMethod.Put, $"/devices/identifier/{identifier}/token", request, true, false); } public Task GetDevicesExistenceByTypes(DeviceType[] deviceTypes) { return SendAsync( HttpMethod.Post, "/devices/exist-by-types", deviceTypes, true, true); } public Task GetDeviceByIdentifierAsync(string deviceIdentifier) { return SendAsync(HttpMethod.Get, $"/devices/identifier/{deviceIdentifier}", null, true, true); } public Task UpdateTrustedDeviceKeysAsync(string deviceIdentifier, TrustedDeviceKeysRequest trustedDeviceKeysRequest) { return SendAsync(HttpMethod.Put, $"/devices/{deviceIdentifier}/keys", trustedDeviceKeysRequest, true, true); } #endregion #region Event APIs public async Task PostEventsCollectAsync(IEnumerable request) { using (var requestMessage = new HttpRequestMessage()) { requestMessage.Version = new Version(1, 0); requestMessage.Method = HttpMethod.Post; requestMessage.RequestUri = new Uri(string.Concat(EventsBaseUrl, "/collect")); var authHeader = await GetActiveBearerTokenAsync(); requestMessage.Headers.Add("Authorization", string.Concat("Bearer ", authHeader)); requestMessage.Content = new StringContent(JsonConvert.SerializeObject(request, _jsonSettings), Encoding.UTF8, "application/json"); HttpResponseMessage response; try { response = await _httpClient.SendAsync(requestMessage); } catch (Exception e) { throw new ApiException(HandleWebError(e)); } if (!response.IsSuccessStatusCode) { var error = await HandleErrorAsync(response, false, false); throw new ApiException(error); } } } #endregion #region HIBP APIs public Task> GetHibpBreachAsync(string username) { return SendAsync>(HttpMethod.Get, string.Concat("/hibp/breach?username=", username), null, true, true); } #endregion #region Organizations APIs public Task GetOrganizationKeysAsync(string id) { return SendAsync(HttpMethod.Get, $"/organizations/{id}/keys", null, true, true); } public Task GetOrganizationAutoEnrollStatusAsync(string identifier) { return SendAsync(HttpMethod.Get, $"/organizations/{identifier}/auto-enroll-status", null, true, true); } public Task PostLeaveOrganizationAsync(string id) { return SendAsync(HttpMethod.Post, $"/organizations/{id}/leave", null, true, false); } public Task GetOrgDomainSsoDetailsAsync(string userEmail) { return SendAsync(HttpMethod.Post, $"/organizations/domain/sso/details", new OrganizationDomainSsoDetailsRequest { Email = userEmail }, false, true); } #endregion #region Organization User APIs public Task PutOrganizationUserResetPasswordEnrollmentAsync(string orgId, string userId, OrganizationUserResetPasswordEnrollmentRequest request) { return SendAsync(HttpMethod.Put, $"/organizations/{orgId}/users/{userId}/reset-password-enrollment", request, true, false); } #endregion #region Key Connector public async Task GetMasterKeyFromKeyConnectorAsync(string keyConnectorUrl) { using (var requestMessage = new HttpRequestMessage()) { var authHeader = await GetActiveBearerTokenAsync(); requestMessage.Version = new Version(1, 0); requestMessage.Method = HttpMethod.Get; requestMessage.RequestUri = new Uri(string.Concat(keyConnectorUrl, "/user-keys")); requestMessage.Headers.Add("Authorization", string.Concat("Bearer ", authHeader)); HttpResponseMessage response; try { response = await _httpClient.SendAsync(requestMessage); } catch (Exception e) { throw new ApiException(HandleWebError(e)); } if (!response.IsSuccessStatusCode) { var error = await HandleErrorAsync(response, false, true); throw new ApiException(error); } var responseJsonString = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(responseJsonString); } } public async Task PostMasterKeyToKeyConnectorAsync(string keyConnectorUrl, KeyConnectorUserKeyRequest request) { using (var requestMessage = new HttpRequestMessage()) { var authHeader = await GetActiveBearerTokenAsync(); requestMessage.Version = new Version(1, 0); requestMessage.Method = HttpMethod.Post; requestMessage.RequestUri = new Uri(string.Concat(keyConnectorUrl, "/user-keys")); requestMessage.Headers.Add("Authorization", string.Concat("Bearer ", authHeader)); requestMessage.Content = new StringContent(JsonConvert.SerializeObject(request, _jsonSettings), Encoding.UTF8, "application/json"); HttpResponseMessage response; try { response = await _httpClient.SendAsync(requestMessage); } catch (Exception e) { throw new ApiException(HandleWebError(e)); } if (!response.IsSuccessStatusCode) { var error = await HandleErrorAsync(response, false, true); throw new ApiException(error); } } } #endregion #region PasswordlessLogin public async Task> GetAuthRequestAsync() { var response = await SendAsync(HttpMethod.Get, $"/auth-requests/", null, true, true); return response.Data; } public Task GetAuthRequestAsync(string id) { return SendAsync(HttpMethod.Get, $"/auth-requests/{id}", null, true, true); } public Task GetAuthResponseAsync(string id, string accessCode) { return SendAsync(HttpMethod.Get, $"/auth-requests/{id}/response?code={accessCode}", null, false, true); } public Task PostCreateRequestAsync(PasswordlessCreateLoginRequest passwordlessCreateLoginRequest, AuthRequestType authRequestType) { return SendAsync(HttpMethod.Post, authRequestType == AuthRequestType.AdminApproval ? "/auth-requests/admin-request" : "/auth-requests", passwordlessCreateLoginRequest, authRequestType == AuthRequestType.AdminApproval, true, (message) => message.Headers.Add("Device-Identifier", passwordlessCreateLoginRequest.DeviceIdentifier)); } public Task PutAuthRequestAsync(string id, string encKey, string encMasterPasswordHash, string deviceIdentifier, bool requestApproved) { var request = new PasswordlessLoginRequest(encKey, encMasterPasswordHash, deviceIdentifier, requestApproved); return SendAsync(HttpMethod.Put, $"/auth-requests/{id}", request, true, true); } #endregion #region Configs public async Task GetConfigsAsync() { var accessToken = await _tokenService.GetTokenAsync(); return await SendAsync(HttpMethod.Get, "/config/", null, !string.IsNullOrEmpty(accessToken), true); } #endregion #region Helpers public async Task GetActiveBearerTokenAsync() { var accessToken = await _tokenService.GetTokenAsync(); if (_tokenService.TokenNeedsRefresh()) { var tokenResponse = await DoRefreshTokenAsync(); accessToken = tokenResponse.AccessToken; } return accessToken; } public async Task PreValidateSsoAsync(string identifier) { var path = "/sso/prevalidate?domainHint=" + WebUtility.UrlEncode(identifier); using (var requestMessage = new HttpRequestMessage()) { requestMessage.Version = new Version(1, 0); requestMessage.Method = HttpMethod.Get; requestMessage.RequestUri = new Uri(string.Concat(IdentityBaseUrl, path)); requestMessage.Headers.Add("Accept", "application/json"); HttpResponseMessage response; try { response = await _httpClient.SendAsync(requestMessage); } catch (Exception e) { throw new ApiException(HandleWebError(e)); } if (!response.IsSuccessStatusCode) { var error = await HandleErrorAsync(response, false, true); throw new ApiException(error); } var responseJsonString = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(responseJsonString); } } public Task SendAsync(HttpMethod method, string path, bool authed) => SendAsync(method, path, null, authed, false); public Task SendAsync(HttpMethod method, string path, TRequest body, bool authed) => SendAsync(method, path, body, authed, false); public Task SendAsync(HttpMethod method, string path, bool authed) => SendAsync(method, path, null, authed, true); public async Task SendAsync(HttpMethod method, string path, TRequest body, bool authed, bool hasResponse, Action alterRequest = null, bool logoutOnUnauthorized = true, bool sendToIdentity = false) { using (var requestMessage = new HttpRequestMessage()) { var baseUrl = sendToIdentity ? IdentityBaseUrl : ApiBaseUrl; requestMessage.Version = new Version(1, 0); requestMessage.Method = method; if (!Uri.IsWellFormedUriString(baseUrl, UriKind.Absolute)) { throw new ApiException(new ErrorResponse { StatusCode = HttpStatusCode.BadGateway, //Note: This message is hardcoded until AppResources.resx gets moved into Core.csproj Message = "One or more URLs saved in the Settings are incorrect. Please revise it and try to log in again." }); } requestMessage.RequestUri = new Uri(string.Concat(baseUrl, path)); if (body != null) { var bodyType = body.GetType(); if (bodyType == typeof(string)) { requestMessage.Content = new StringContent((object)bodyType as string, Encoding.UTF8, "application/x-www-form-urlencoded; charset=utf-8"); } else if (bodyType == typeof(MultipartFormDataContent)) { requestMessage.Content = body as MultipartFormDataContent; } else { requestMessage.Content = new StringContent(JsonConvert.SerializeObject(body, _jsonSettings), Encoding.UTF8, "application/json"); } } if (authed) { var authHeader = await GetActiveBearerTokenAsync(); requestMessage.Headers.Add("Authorization", string.Concat("Bearer ", authHeader)); } if (hasResponse) { requestMessage.Headers.Add("Accept", "application/json"); } alterRequest?.Invoke(requestMessage); HttpResponseMessage response; try { response = await _httpClient.SendAsync(requestMessage); } catch (Exception e) { throw new ApiException(HandleWebError(e)); } if (hasResponse && response.IsSuccessStatusCode) { var responseJsonString = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(responseJsonString); } else if (!response.IsSuccessStatusCode) { var error = await HandleErrorAsync(response, false, authed, logoutOnUnauthorized); throw new ApiException(error); } return (TResponse)(object)null; } } public async Task DoRefreshTokenAsync() { var refreshToken = await _tokenService.GetRefreshTokenAsync(); if (string.IsNullOrWhiteSpace(refreshToken)) { throw new ApiException(); } var decodedToken = _tokenService.DecodeToken(); var requestMessage = new HttpRequestMessage { Version = new Version(1, 0), RequestUri = new Uri(string.Concat(IdentityBaseUrl, "/connect/token")), Method = HttpMethod.Post, Content = new FormUrlEncodedContent(new Dictionary { ["grant_type"] = "refresh_token", ["client_id"] = decodedToken.GetValue("client_id")?.Value(), ["refresh_token"] = refreshToken }) }; requestMessage.Headers.Add("Accept", "application/json"); HttpResponseMessage response; try { response = await _httpClient.SendAsync(requestMessage); } catch (Exception e) { throw new ApiException(HandleWebError(e)); } if (response.IsSuccessStatusCode) { var responseJsonString = await response.Content.ReadAsStringAsync(); var tokenResponse = JsonConvert.DeserializeObject(responseJsonString); await _tokenService.SetTokensAsync(tokenResponse.AccessToken, tokenResponse.RefreshToken); return tokenResponse; } else { var error = await HandleErrorAsync(response, true, true); throw new ApiException(error); } } public async Task SendAsync(HttpRequestMessage requestMessage, CancellationToken cancellationToken = default) { HttpResponseMessage response; try { response = await _httpClient.SendAsync(requestMessage, cancellationToken); } catch (Exception e) { throw new ApiException(HandleWebError(e)); } if (!response.IsSuccessStatusCode) { throw new ApiException(new ErrorResponse { StatusCode = response.StatusCode, Message = $"{requestMessage.RequestUri} error: {(int)response.StatusCode} {response.ReasonPhrase}." }); } return response; } public async Task GetFastmailAccountIdAsync(string apiKey) { using (var httpclient = new HttpClient()) { HttpResponseMessage response; try { httpclient.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}"); httpclient.DefaultRequestHeaders.Add("Accept", "application/json"); response = await httpclient.GetAsync(new Uri("https://api.fastmail.com/jmap/session")); } catch (Exception e) { throw new ApiException(HandleWebError(e)); } if (!response.IsSuccessStatusCode) { throw new ApiException(new ErrorResponse { StatusCode = response.StatusCode, Message = $"Fastmail error: {(int)response.StatusCode} {response.ReasonPhrase}." }); } var result = JObject.Parse(await response.Content.ReadAsStringAsync()); return result["primaryAccounts"]?["https://www.fastmail.com/dev/maskedemail"]?.ToString(); } } public async Task> GetDigitalAssetLinksForRpAsync(string rpId) { using (var httpclient = new HttpClient()) { HttpResponseMessage response; try { httpclient.DefaultRequestHeaders.Add("Accept", "application/json"); response = await httpclient.GetAsync(new Uri($"https://{rpId}/.well-known/assetlinks.json")); } catch (Exception e) { throw new ApiException(HandleWebError(e)); } if (!response.IsSuccessStatusCode) { throw new ApiException(new ErrorResponse { StatusCode = response.StatusCode, Message = $"Digital Asset links Rp error: {(int)response.StatusCode} {response.ReasonPhrase}." }); } var json = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject>(json); } } private ErrorResponse HandleWebError(Exception e) { return new ErrorResponse { StatusCode = HttpStatusCode.BadGateway, Message = "Exception message: " + e.Message }; } private async Task HandleErrorAsync(HttpResponseMessage response, bool tokenError, bool authed, bool logoutOnUnauthorized = true) { if (authed && ( (logoutOnUnauthorized && response.StatusCode == HttpStatusCode.Unauthorized) || response.StatusCode == HttpStatusCode.Forbidden )) { await _logoutCallbackAsync(new Tuple(null, false, true)); return null; } try { JObject responseJObject = null; if (IsJsonResponse(response)) { var responseJsonString = await response.Content.ReadAsStringAsync(); responseJObject = JObject.Parse(responseJsonString); } if (authed && tokenError && response.StatusCode == HttpStatusCode.BadRequest && responseJObject?["error"]?.ToString() == "invalid_grant") { await _logoutCallbackAsync(new Tuple(null, false, true)); return null; } return new ErrorResponse(responseJObject, response.StatusCode, tokenError); } catch { return null; } } private bool IsJsonResponse(HttpResponseMessage response) { if (response.Content?.Headers is null) { return false; } if (response.Content.Headers.ContentType?.MediaType == "application/json") { return true; } return response.Content.Headers.TryGetValues("Content-Type", out var vals) && vals?.Any(v => v.Contains("application/json")) is true; } #endregion } }