mirror of
https://github.com/bitwarden/server.git
synced 2025-04-25 06:42:22 -05:00

* Get limited life attachment download URL This change limits url download to a 1min lifetime. This requires moving to a new container to allow for non-public blob access. Clients will have to call GetAttachmentData api function to receive the download URL. For backwards compatibility, attachment URLs are still present, but will not work for attachments stored in non-public access blobs. * Make GlobalSettings interface for testing * Test LocalAttachmentStorageService equivalence * Remove comment * Add missing globalSettings using * Simplify default attachment container * Default to attachments containe for existing methods A new upload method will be made for uploading to attachments-v2. For compatibility for clients which don't use these new methods, we need to still use the old container. The new container will be used only for new uploads * Remove Default MetaData fixture. * Keep attachments container blob-level security for all instances * Close unclosed FileStream * Favor default value for noop services
128 lines
5.4 KiB
C#
128 lines
5.4 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Threading.Tasks;
|
|
using Bit.Billing.Models;
|
|
using Bit.Core.Repositories;
|
|
using Bit.Core.Settings;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
namespace Bit.Core.Services
|
|
{
|
|
public class AppleIapService : IAppleIapService
|
|
{
|
|
private readonly HttpClient _httpClient = new HttpClient();
|
|
|
|
private readonly GlobalSettings _globalSettings;
|
|
private readonly IWebHostEnvironment _hostingEnvironment;
|
|
private readonly IMetaDataRepository _metaDataRespository;
|
|
private readonly ILogger<AppleIapService> _logger;
|
|
|
|
public AppleIapService(
|
|
GlobalSettings globalSettings,
|
|
IWebHostEnvironment hostingEnvironment,
|
|
IMetaDataRepository metaDataRespository,
|
|
ILogger<AppleIapService> logger)
|
|
{
|
|
_globalSettings = globalSettings;
|
|
_hostingEnvironment = hostingEnvironment;
|
|
_metaDataRespository = metaDataRespository;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<AppleReceiptStatus> GetVerifiedReceiptStatusAsync(string receiptData)
|
|
{
|
|
var receiptStatus = await GetReceiptStatusAsync(receiptData);
|
|
if (receiptStatus?.Status != 0)
|
|
{
|
|
return null;
|
|
}
|
|
var validEnvironment = _globalSettings.AppleIap.AppInReview ||
|
|
(!_hostingEnvironment.IsProduction() && receiptStatus.Environment == "Sandbox") ||
|
|
(_hostingEnvironment.IsProduction() && receiptStatus.Environment != "Sandbox");
|
|
var validProductBundle = receiptStatus.Receipt.BundleId == "com.bitwarden.desktop" ||
|
|
receiptStatus.Receipt.BundleId == "com.8bit.bitwarden";
|
|
var validProduct = receiptStatus.LatestReceiptInfo.LastOrDefault()?.ProductId == "premium_annually";
|
|
var validIds = receiptStatus.GetOriginalTransactionId() != null &&
|
|
receiptStatus.GetLastTransactionId() != null;
|
|
var validTransaction = receiptStatus.GetLastExpiresDate()
|
|
.GetValueOrDefault(DateTime.MinValue) > DateTime.UtcNow;
|
|
if (validEnvironment && validProductBundle && validProduct && validIds && validTransaction)
|
|
{
|
|
return receiptStatus;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public async Task SaveReceiptAsync(AppleReceiptStatus receiptStatus, Guid userId)
|
|
{
|
|
var originalTransactionId = receiptStatus.GetOriginalTransactionId();
|
|
if (string.IsNullOrWhiteSpace(originalTransactionId))
|
|
{
|
|
throw new Exception("OriginalTransactionId is null");
|
|
}
|
|
await _metaDataRespository.UpsertAsync("AppleReceipt", originalTransactionId,
|
|
new Dictionary<string, string>
|
|
{
|
|
["Data"] = receiptStatus.GetReceiptData(),
|
|
["UserId"] = userId.ToString()
|
|
});
|
|
}
|
|
|
|
public async Task<Tuple<string, Guid?>> GetReceiptAsync(string originalTransactionId)
|
|
{
|
|
var receipt = await _metaDataRespository.GetAsync("AppleReceipt", originalTransactionId);
|
|
if (receipt == null)
|
|
{
|
|
return null;
|
|
}
|
|
return new Tuple<string, Guid?>(receipt.ContainsKey("Data") ? receipt["Data"] : null,
|
|
receipt.ContainsKey("UserId") ? new Guid(receipt["UserId"]) : (Guid?)null);
|
|
}
|
|
|
|
private async Task<AppleReceiptStatus> GetReceiptStatusAsync(string receiptData, bool prod = true,
|
|
int attempt = 0, AppleReceiptStatus lastReceiptStatus = null)
|
|
{
|
|
try
|
|
{
|
|
if (attempt > 4)
|
|
{
|
|
throw new Exception("Failed verifying Apple IAP after too many attempts. Last attempt status: " +
|
|
lastReceiptStatus?.Status ?? "null");
|
|
}
|
|
|
|
var url = string.Format("https://{0}.itunes.apple.com/verifyReceipt", prod ? "buy" : "sandbox");
|
|
var json = new JObject(new JProperty("receipt-data", receiptData),
|
|
new JProperty("password", _globalSettings.AppleIap.Password)).ToString();
|
|
|
|
var response = await _httpClient.PostAsync(url, new StringContent(json));
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
var responseJson = await response.Content.ReadAsStringAsync();
|
|
var receiptStatus = JsonConvert.DeserializeObject<AppleReceiptStatus>(responseJson);
|
|
if (receiptStatus.Status == 21007)
|
|
{
|
|
return await GetReceiptStatusAsync(receiptData, false, attempt + 1, receiptStatus);
|
|
}
|
|
else if (receiptStatus.Status == 21005)
|
|
{
|
|
await Task.Delay(2000);
|
|
return await GetReceiptStatusAsync(receiptData, prod, attempt + 1, receiptStatus);
|
|
}
|
|
return receiptStatus;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_logger.LogWarning(e, "Error verifying Apple IAP receipt.");
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
}
|