package economy import ( "context" "crypto" "crypto/rsa" "crypto/sha1" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "fmt" "net/http" "strings" "time" ) // AdVerifier تأیید می‌کند که کاربر یک تبلیغ rewarded را کامل دیده است. 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, p IAPProof) (bool, error) } // --- استاب توسعه: هر توکن غیرخالی را معتبر می‌داند (فقط برای محیط dev/تست) --- type DevAdVerifier struct{} func (DevAdVerifier) Verify(_ context.Context, _ int64, token string) (bool, error) { return token != "", nil } type DevIAPVerifier struct{} func (DevIAPVerifier) Verify(_ context.Context, p IAPProof) (bool, error) { return p.Token != "", nil } // --- آداپتر تپسل (تأیید سمت‌سرور تبلیغ rewarded) --- // // تپسل پس از پایان کامل تبلیغ، یک توکن/شناسه می‌دهد که سرور باید آن را // نزد تپسل اعتبارسنجی کند. آدرس و قالب دقیق بسته به پنل تپسل تنظیم می‌شود؛ // اینجا اسکلت فراخوانی با کلید قرار داده شده است. type TapsellAdVerifier struct { APIKey string BaseURL string // پیش‌فرض اگر خالی باشد ست می‌شود client *http.Client } func NewTapsellAdVerifier(apiKey string) *TapsellAdVerifier { return &TapsellAdVerifier{ APIKey: apiKey, BaseURL: "https://api.tapsell.ir", client: &http.Client{Timeout: 5 * time.Second}, } } func (t *TapsellAdVerifier) Verify(ctx context.Context, userID int64, token string) (bool, error) { if t.APIKey == "" || token == "" { return false, fmt.Errorf("tapsell: missing api key or token") } // TODO: مسیر دقیق اعتبارسنجی تپسل را مطابق مستندات پنل تنظیم کنید. url := fmt.Sprintf("%s/rewarded/verify?token=%s", t.BaseURL, token) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return false, err } req.Header.Set("Authorization", t.APIKey) resp, err := t.client.Do(req) if err != nil { return false, err } defer resp.Body.Close() var out struct { Valid bool `json:"valid"` Completed bool `json:"completed"` } if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return false, err } return out.Valid && out.Completed, nil } // --- مایکت: تأیید سمت‌سرور با X-Access-Token --- // // GET https://developer.myket.ir/api/applications/{pkg}/purchases/products/{sku}/tokens/{token} // هدر: X-Access-Token: . پاسخ purchaseState=0 یعنی معتبر (غیرِ صفر=مسترد). type MyketVerifier struct { PackageName string AccessToken string client *http.Client } func NewMyketVerifier(pkg, accessToken string) *MyketVerifier { return &MyketVerifier{ 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") } 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("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 // ۴۰۴/۴۰۱ ⇒ توکنِ نامعتبر } var out struct { 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) }