feat: add uuid
This commit is contained in:
+130
-38
@@ -2,9 +2,16 @@ package economy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -13,9 +20,20 @@ type AdVerifier interface {
|
||||
Verify(ctx context.Context, userID int64, token string) (bool, error)
|
||||
}
|
||||
|
||||
// IAPProof دادههای لازم برای تأیید یک خرید درونبرنامهای.
|
||||
// برای مایکت: Store/ProductID/Token کافی است (تأیید سمتسرور با access token).
|
||||
// برای کافهبازار: PurchaseData + Signature برای تأیید امضای RSA آفلاین.
|
||||
type IAPProof struct {
|
||||
Store string
|
||||
ProductID string
|
||||
Token string
|
||||
PurchaseData string
|
||||
Signature string
|
||||
}
|
||||
|
||||
// IAPVerifier تأیید میکند که یک خرید درونبرنامهای معتبر و پرداختشده است.
|
||||
type IAPVerifier interface {
|
||||
Verify(ctx context.Context, store, productID, purchaseToken string) (bool, error)
|
||||
Verify(ctx context.Context, p IAPProof) (bool, error)
|
||||
}
|
||||
|
||||
// --- استاب توسعه: هر توکن غیرخالی را معتبر میداند (فقط برای محیط dev/تست) ---
|
||||
@@ -28,8 +46,8 @@ func (DevAdVerifier) Verify(_ context.Context, _ int64, token string) (bool, err
|
||||
|
||||
type DevIAPVerifier struct{}
|
||||
|
||||
func (DevIAPVerifier) Verify(_ context.Context, _ string, _ string, token string) (bool, error) {
|
||||
return token != "", nil
|
||||
func (DevIAPVerifier) Verify(_ context.Context, p IAPProof) (bool, error) {
|
||||
return p.Token != "", nil
|
||||
}
|
||||
|
||||
// --- آداپتر تپسل (تأیید سمتسرور تبلیغ rewarded) ---
|
||||
@@ -77,63 +95,137 @@ func (t *TapsellAdVerifier) Verify(ctx context.Context, userID int64, token stri
|
||||
return out.Valid && out.Completed, nil
|
||||
}
|
||||
|
||||
// --- آداپتر IAP کافهبازار / مایکت ---
|
||||
// --- مایکت: تأیید سمتسرور با X-Access-Token ---
|
||||
//
|
||||
// هر دو فروشگاه API شبیه Google Play دارند: با access_token (که از refresh_token
|
||||
// گرفته میشود) وضعیت یک purchaseToken برای یک محصول را اعتبارسنجی میکنیم.
|
||||
type StoreIAPVerifier struct {
|
||||
// قالب آدرس اعتبارسنجی؛ %s ها به ترتیب: package, product, token
|
||||
ValidateURLFmt string
|
||||
PackageName string
|
||||
AccessToken string // در عمل باید از refresh_token تازهسازی شود
|
||||
client *http.Client
|
||||
// GET https://developer.myket.ir/api/applications/{pkg}/purchases/products/{sku}/tokens/{token}
|
||||
// هدر: X-Access-Token: <accessToken> . پاسخ purchaseState=0 یعنی معتبر (غیرِ صفر=مسترد).
|
||||
type MyketVerifier struct {
|
||||
PackageName string
|
||||
AccessToken string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewBazaarVerifier آداپتر کافهبازار.
|
||||
func NewBazaarVerifier(pkg, accessToken string) *StoreIAPVerifier {
|
||||
return &StoreIAPVerifier{
|
||||
ValidateURLFmt: "https://pardakht.cafebazaar.ir/devapi/v2/api/validate/%s/inapp/%s/purchases/%s/",
|
||||
PackageName: pkg,
|
||||
AccessToken: accessToken,
|
||||
client: &http.Client{Timeout: 6 * time.Second},
|
||||
func NewMyketVerifier(pkg, accessToken string) *MyketVerifier {
|
||||
return &MyketVerifier{
|
||||
PackageName: pkg,
|
||||
AccessToken: accessToken,
|
||||
client: &http.Client{Timeout: 6 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
// NewMyketVerifier آداپتر مایکت.
|
||||
func NewMyketVerifier(pkg, accessToken string) *StoreIAPVerifier {
|
||||
return &StoreIAPVerifier{
|
||||
ValidateURLFmt: "https://developer.myket.ir/api/applications/%s/purchases/products/%s/tokens/%s/",
|
||||
PackageName: pkg,
|
||||
AccessToken: accessToken,
|
||||
client: &http.Client{Timeout: 6 * time.Second},
|
||||
func (v *MyketVerifier) Verify(ctx context.Context, p IAPProof) (bool, error) {
|
||||
if v.AccessToken == "" || p.Token == "" {
|
||||
return false, fmt.Errorf("myket: missing access token or purchase token")
|
||||
}
|
||||
}
|
||||
|
||||
func (v *StoreIAPVerifier) Verify(ctx context.Context, _ string, productID, purchaseToken string) (bool, error) {
|
||||
if v.AccessToken == "" || purchaseToken == "" {
|
||||
return false, fmt.Errorf("iap: missing access token or purchase token")
|
||||
}
|
||||
url := fmt.Sprintf(v.ValidateURLFmt, v.PackageName, productID, purchaseToken)
|
||||
url := fmt.Sprintf(
|
||||
"https://developer.myket.ir/api/applications/%s/purchases/products/%s/tokens/%s",
|
||||
v.PackageName, p.ProductID, p.Token)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+v.AccessToken)
|
||||
req.Header.Set("X-Access-Token", v.AccessToken)
|
||||
resp, err := v.client.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return false, nil
|
||||
return false, nil // ۴۰۴/۴۰۱ ⇒ توکنِ نامعتبر
|
||||
}
|
||||
var out struct {
|
||||
// هر دو فروشگاه فیلدی شبیه purchaseState برمیگردانند (۰ = خریداریشده)
|
||||
PurchaseState int `json:"purchaseState"`
|
||||
ConsumptionState int `json:"consumptionState"`
|
||||
PurchaseState int `json:"purchaseState"` // ۰=خریداریشده، غیرِصفر=مسترد
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return out.PurchaseState == 0, nil
|
||||
}
|
||||
|
||||
// --- کافهبازار: تأیید امضای RSA آفلاین (فقط کلید عمومی لازم است) ---
|
||||
//
|
||||
// کلاینت purchaseData (متنِ خامِ JSON) و signature (base64) را میفرستد؛ صحتِ امضا
|
||||
// با کلیدِ عمومیِ RSA و الگوریتم SHA1withRSA (مانند Google Play/Bazaar) بررسی میشود.
|
||||
type BazaarRSAVerifier struct {
|
||||
pub *rsa.PublicKey
|
||||
}
|
||||
|
||||
// NewBazaarRSAVerifier کلیدِ عمومی را از رشتهی base64 (DER/PKIX) یا PEM میپذیرد.
|
||||
func NewBazaarRSAVerifier(publicKey string) (*BazaarRSAVerifier, error) {
|
||||
pub, err := parseRSAPublicKey(publicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &BazaarRSAVerifier{pub: pub}, nil
|
||||
}
|
||||
|
||||
func (v *BazaarRSAVerifier) Verify(_ context.Context, p IAPProof) (bool, error) {
|
||||
if p.PurchaseData == "" || p.Signature == "" {
|
||||
return false, fmt.Errorf("bazaar: missing purchase data or signature")
|
||||
}
|
||||
sig, err := base64.StdEncoding.DecodeString(p.Signature)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("bazaar: bad signature encoding: %w", err)
|
||||
}
|
||||
h := sha1.Sum([]byte(p.PurchaseData))
|
||||
if err := rsa.VerifyPKCS1v15(v.pub, crypto.SHA1, h[:], sig); err != nil {
|
||||
return false, nil // امضای نامعتبر ⇒ خریدِ نامعتبر (نه خطای سرور)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// parseRSAPublicKey کلیدِ عمومیِ RSA را از PEM یا base64ِ خامِ DER (خروجیِ کنسولِ
|
||||
// توسعهدهنده) استخراج میکند.
|
||||
func parseRSAPublicKey(key string) (*rsa.PublicKey, error) {
|
||||
key = strings.TrimSpace(key)
|
||||
var der []byte
|
||||
if strings.Contains(key, "BEGIN") {
|
||||
block, _ := pem.Decode([]byte(key))
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("rsa: invalid PEM")
|
||||
}
|
||||
der = block.Bytes
|
||||
} else {
|
||||
clean := strings.NewReplacer("\n", "", "\r", "", " ", "").Replace(key)
|
||||
b, err := base64.StdEncoding.DecodeString(clean)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rsa: invalid base64 key: %w", err)
|
||||
}
|
||||
der = b
|
||||
}
|
||||
pub, err := x509.ParsePKIXPublicKey(der)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rsa: parse key: %w", err)
|
||||
}
|
||||
rsaPub, ok := pub.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("rsa: not an RSA public key")
|
||||
}
|
||||
return rsaPub, nil
|
||||
}
|
||||
|
||||
// StoreRouter بر اساس store، تأیید را به مایکت یا کافهبازار میسپارد.
|
||||
// اگر فروشگاهی پیکربندی نشده باشد و devFallback روشن باشد، از DevIAPVerifier
|
||||
// استفاده میشود (برای توسعه/تست).
|
||||
type StoreRouter struct {
|
||||
Myket IAPVerifier
|
||||
Bazaar IAPVerifier
|
||||
DevFallback bool
|
||||
}
|
||||
|
||||
func (r StoreRouter) Verify(ctx context.Context, p IAPProof) (bool, error) {
|
||||
var v IAPVerifier
|
||||
switch p.Store {
|
||||
case "myket":
|
||||
v = r.Myket
|
||||
case "bazaar", "cafe", "cafebazaar":
|
||||
v = r.Bazaar
|
||||
}
|
||||
if v == nil {
|
||||
if r.DevFallback {
|
||||
return DevIAPVerifier{}.Verify(ctx, p)
|
||||
}
|
||||
return false, fmt.Errorf("iap: store %q not configured", p.Store)
|
||||
}
|
||||
return v.Verify(ctx, p)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user