using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace JiDangJia.Server;
// Only the server owns payment keys. Prices and entitlement always come from the server catalog.
public sealed class AlipayGateway : IDisposable
{
private const string Gateway="https://openapi.alipay.com/gateway.do";
private readonly object sync=new();
private readonly RSA? merchant,alipay;
private readonly string appId="",sellerId="",notifyUrl="",returnUrl="";
public bool Enabled {get;}
public string Message=>Enabled?"支付宝付款成功后自动发放卡密":"支付宝暂未开通,请扫码联系客服购买";
public AlipayGateway(IConfiguration config)
{
if(!bool.TryParse(config["JDJ_ALIPAY_ENABLED"],out var enabled)||!enabled)return;
try
{
appId=config["JDJ_ALIPAY_APP_ID"]??"";sellerId=config["JDJ_ALIPAY_SELLER_ID"]??"";
if(!Regex.IsMatch(appId,@"^\d{16,32}$")||!Regex.IsMatch(sellerId,@"^\d{16,28}$"))return;
if(!Uri.TryCreate(config["JDJ_ALIPAY_PUBLIC_BASE_URL"],UriKind.Absolute,out var root)||root.Scheme is not ("http" or "https")||root.IsLoopback||root.UserInfo.Length>0||root.Query.Length>0||root.Fragment.Length>0||root.AbsolutePath!="/")return;
notifyUrl=new Uri(root,"api/purchase/alipay/notify").AbsoluteUri;returnUrl=new Uri(root,"purchase/return").AbsoluteUri;
merchant=RSA.Create();merchant.ImportFromPem(File.ReadAllText(PrivateStorage.OutsideArtifacts(config["JDJ_ALIPAY_PRIVATE_KEY_FILE"]??"")));
alipay=RSA.Create();alipay.ImportFromPem(File.ReadAllText(PrivateStorage.OutsideArtifacts(config["JDJ_ALIPAY_PUBLIC_KEY_FILE"]??"")));
if(merchant.KeySize<2048||alipay.KeySize<2048)return;
merchant.SignData("configuration-check"u8,HashAlgorithmName.SHA256,RSASignaturePadding.Pkcs1);
Enabled=true;
}
catch(Exception ex)when(ex is IOException or UnauthorizedAccessException or ArgumentException or CryptographicException){Enabled=false;}
}
private static string Canonical(IReadOnlyDictionary<string,string> values,bool notification=false)=>string.Join("&",values.Where(p=>p.Value.Length>0&&p.Key!="sign"&&(!notification||p.Key!="sign_type")).OrderBy(p=>p.Key,StringComparer.Ordinal).Select(p=>p.Key+"="+p.Value));
public string Checkout(string id,int amountCents,DateTimeOffset expires)
{
if(!Enabled)throw new CommunityException(503,Message);
var values=new Dictionary<string,string>(StringComparer.Ordinal)
{
["app_id"]=appId,["method"]="alipay.trade.wap.pay",["format"]="JSON",["charset"]="utf-8",["sign_type"]="RSA2",
["timestamp"]=DateTimeOffset.UtcNow.ToOffset(TimeSpan.FromHours(8)).ToString("yyyy-MM-dd HH:mm:ss",CultureInfo.InvariantCulture),["version"]="1.0",
["notify_url"]=notifyUrl,["return_url"]=returnUrl,
["biz_content"]=JsonSerializer.Serialize(new{out_trade_no=id,total_amount=(amountCents/100m).ToString("0.00",CultureInfo.InvariantCulture),subject=ServerProduct.Name+"卡密授权",seller_id=sellerId,product_code="QUICK_WAP_PAY",goods_type="0",time_expire=expires.ToOffset(TimeSpan.FromHours(8)).ToString("yyyy-MM-dd HH:mm",CultureInfo.InvariantCulture),quit_url=returnUrl})
};
lock(sync)values["sign"]=Convert.ToBase64String(merchant!.SignData(Encoding.UTF8.GetBytes(Canonical(values)),HashAlgorithmName.SHA256,RSASignaturePadding.Pkcs1));
return Gateway+"?"+string.Join("&",values.OrderBy(p=>p.Key,StringComparer.Ordinal).Select(p=>Uri.EscapeDataString(p.Key)+"="+Uri.EscapeDataString(p.Value)));
}
public bool Verify(IReadOnlyDictionary<string,string> values)
{
if(!Enabled||values.Count>80||values.Any(p=>p.Key.Length>100||p.Value.Length>16384)||values.GetValueOrDefault("sign_type")!="RSA2"||values.GetValueOrDefault("app_id")!=appId||values.GetValueOrDefault("seller_id")!=sellerId||values.TryGetValue("charset",out var charset)&&!charset.Equals("utf-8",StringComparison.OrdinalIgnoreCase))return false;
try
{
var signature=Convert.FromBase64String(values.GetValueOrDefault("sign")??"");
lock(sync)return alipay!.VerifyData(Encoding.UTF8.GetBytes(Canonical(values,true)),signature,HashAlgorithmName.SHA256,RSASignaturePadding.Pkcs1);
}
catch(Exception ex)when(ex is FormatException or CryptographicException){return false;}
}
public void Dispose(){merchant?.Dispose();alipay?.Dispose();}
}
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace JiDangJia.Server;
// Only the server owns payment keys. Prices and entitlement always come from the server catalog.
public sealed class AlipayGateway : IDisposable
{
private const string Gateway="https://openapi.alipay.com/gateway.do";
private readonly object sync=new();
private readonly RSA? merchant,alipay;
private readonly string appId="",sellerId="",notifyUrl="",returnUrl="";
public bool Enabled {get;}
public string Message=>Enabled?"支付宝付款成功后自动发放卡密":"支付宝暂未开通,请扫码联系客服购买";
public AlipayGateway(IConfiguration config)
{
if(!bool.TryParse(config["JDJ_ALIPAY_ENABLED"],out var enabled)||!enabled)return;
try
{
appId=config["JDJ_ALIPAY_APP_ID"]??"";sellerId=config["JDJ_ALIPAY_SELLER_ID"]??"";
if(!Regex.IsMatch(appId,@"^\d{16,32}$")||!Regex.IsMatch(sellerId,@"^\d{16,28}$"))return;
if(!Uri.TryCreate(config["JDJ_ALIPAY_PUBLIC_BASE_URL"],UriKind.Absolute,out var root)||root.Scheme is not ("http" or "https")||root.IsLoopback||root.UserInfo.Length>0||root.Query.Length>0||root.Fragment.Length>0||root.AbsolutePath!="/")return;
notifyUrl=new Uri(root,"api/purchase/alipay/notify").AbsoluteUri;returnUrl=new Uri(root,"purchase/return").AbsoluteUri;
merchant=RSA.Create();merchant.ImportFromPem(File.ReadAllText(PrivateStorage.OutsideArtifacts(config["JDJ_ALIPAY_PRIVATE_KEY_FILE"]??"")));
alipay=RSA.Create();alipay.ImportFromPem(File.ReadAllText(PrivateStorage.OutsideArtifacts(config["JDJ_ALIPAY_PUBLIC_KEY_FILE"]??"")));
if(merchant.KeySize<2048||alipay.KeySize<2048)return;
merchant.SignData("configuration-check"u8,HashAlgorithmName.SHA256,RSASignaturePadding.Pkcs1);
Enabled=true;
}
catch(Exception ex)when(ex is IOException or UnauthorizedAccessException or ArgumentException or CryptographicException){Enabled=false;}
}
private static string Canonical(IReadOnlyDictionary<string,string> values,bool notification=false)=>string.Join("&",values.Where(p=>p.Value.Length>0&&p.Key!="sign"&&(!notification||p.Key!="sign_type")).OrderBy(p=>p.Key,StringComparer.Ordinal).Select(p=>p.Key+"="+p.Value));
public string Checkout(string id,int amountCents,DateTimeOffset expires)
{
if(!Enabled)throw new CommunityException(503,Message);
var values=new Dictionary<string,string>(StringComparer.Ordinal)
{
["app_id"]=appId,["method"]="alipay.trade.wap.pay",["format"]="JSON",["charset"]="utf-8",["sign_type"]="RSA2",
["timestamp"]=DateTimeOffset.UtcNow.ToOffset(TimeSpan.FromHours(8)).ToString("yyyy-MM-dd HH:mm:ss",CultureInfo.InvariantCulture),["version"]="1.0",
["notify_url"]=notifyUrl,["return_url"]=returnUrl,
["biz_content"]=JsonSerializer.Serialize(new{out_trade_no=id,total_amount=(amountCents/100m).ToString("0.00",CultureInfo.InvariantCulture),subject=ServerProduct.Name+"卡密授权",seller_id=sellerId,product_code="QUICK_WAP_PAY",goods_type="0",time_expire=expires.ToOffset(TimeSpan.FromHours(8)).ToString("yyyy-MM-dd HH:mm",CultureInfo.InvariantCulture),quit_url=returnUrl})
};
lock(sync)values["sign"]=Convert.ToBase64String(merchant!.SignData(Encoding.UTF8.GetBytes(Canonical(values)),HashAlgorithmName.SHA256,RSASignaturePadding.Pkcs1));
return Gateway+"?"+string.Join("&",values.OrderBy(p=>p.Key,StringComparer.Ordinal).Select(p=>Uri.EscapeDataString(p.Key)+"="+Uri.EscapeDataString(p.Value)));
}
public bool Verify(IReadOnlyDictionary<string,string> values)
{
if(!Enabled||values.Count>80||values.Any(p=>p.Key.Length>100||p.Value.Length>16384)||values.GetValueOrDefault("sign_type")!="RSA2"||values.GetValueOrDefault("app_id")!=appId||values.GetValueOrDefault("seller_id")!=sellerId||values.TryGetValue("charset",out var charset)&&!charset.Equals("utf-8",StringComparison.OrdinalIgnoreCase))return false;
try
{
var signature=Convert.FromBase64String(values.GetValueOrDefault("sign")??"");
lock(sync)return alipay!.VerifyData(Encoding.UTF8.GetBytes(Canonical(values,true)),signature,HashAlgorithmName.SHA256,RSASignaturePadding.Pkcs1);
}
catch(Exception ex)when(ex is FormatException or CryptographicException){return false;}
}
public void Dispose(){merchant?.Dispose();alipay?.Dispose();}
}