waffo_pancake.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. package service
  2. import (
  3. "bytes"
  4. "context"
  5. "crypto"
  6. "crypto/rsa"
  7. "crypto/sha256"
  8. "crypto/x509"
  9. "encoding/base64"
  10. "encoding/pem"
  11. "fmt"
  12. "io"
  13. "math"
  14. "net/http"
  15. "strconv"
  16. "strings"
  17. "time"
  18. "github.com/QuantumNous/new-api/common"
  19. "github.com/QuantumNous/new-api/dto"
  20. "github.com/QuantumNous/new-api/model"
  21. "github.com/QuantumNous/new-api/setting"
  22. )
  23. const (
  24. waffoPancakeAuthBaseURL = "https://waffo-pancake-auth-service.vercel.app"
  25. waffoPancakeCheckoutPath = "/v1/actions/checkout/create-session"
  26. waffoPancakeDefaultTolerance = 5 * time.Minute
  27. )
  28. type WaffoPancakePriceSnapshot struct {
  29. Amount string `json:"amount"`
  30. TaxIncluded bool `json:"taxIncluded"`
  31. TaxCategory string `json:"taxCategory"`
  32. }
  33. type WaffoPancakeCreateSessionParams struct {
  34. StoreID string `json:"storeId"`
  35. ProductID string `json:"productId"`
  36. ProductType string `json:"productType"`
  37. Currency string `json:"currency"`
  38. PriceSnapshot *WaffoPancakePriceSnapshot `json:"priceSnapshot,omitempty"`
  39. BuyerEmail string `json:"buyerEmail,omitempty"`
  40. SuccessURL string `json:"successUrl,omitempty"`
  41. ExpiresInSeconds *int `json:"expiresInSeconds,omitempty"`
  42. }
  43. type WaffoPancakeCheckoutSession struct {
  44. SessionID string `json:"sessionId"`
  45. CheckoutURL string `json:"checkoutUrl"`
  46. ExpiresAt string `json:"expiresAt"`
  47. OrderID string `json:"orderId"`
  48. }
  49. type waffoPancakeAPIError struct {
  50. Message string `json:"message"`
  51. Layer string `json:"layer"`
  52. }
  53. type waffoPancakeCreateSessionResponse struct {
  54. Data *WaffoPancakeCheckoutSession `json:"data"`
  55. Errors []waffoPancakeAPIError `json:"errors"`
  56. }
  57. type waffoPancakeWebhookData struct {
  58. ID string `json:"id"`
  59. OrderID string `json:"orderId"`
  60. BuyerEmail string `json:"buyerEmail"`
  61. Currency string `json:"currency"`
  62. Amount dto.StringValue `json:"amount"`
  63. TaxAmount dto.StringValue `json:"taxAmount"`
  64. ProductName string `json:"productName"`
  65. }
  66. type waffoPancakeWebhookEvent struct {
  67. ID string `json:"id"`
  68. Timestamp string `json:"timestamp"`
  69. EventType string `json:"eventType"`
  70. EventID string `json:"eventId"`
  71. StoreID string `json:"storeId"`
  72. Mode string `json:"mode"`
  73. Data waffoPancakeWebhookData `json:"data"`
  74. }
  75. func (e *waffoPancakeWebhookEvent) NormalizedEventType() string {
  76. if e == nil {
  77. return ""
  78. }
  79. return e.EventType
  80. }
  81. func CreateWaffoPancakeCheckoutSession(ctx context.Context, params *WaffoPancakeCreateSessionParams) (*WaffoPancakeCheckoutSession, error) {
  82. if params == nil {
  83. return nil, fmt.Errorf("missing checkout params")
  84. }
  85. body, err := common.Marshal(params)
  86. if err != nil {
  87. return nil, fmt.Errorf("marshal Waffo Pancake checkout payload: %w", err)
  88. }
  89. privateKey, err := normalizeRSAPrivateKey(setting.WaffoPancakePrivateKey)
  90. if err != nil {
  91. return nil, err
  92. }
  93. timestamp := strconv.FormatInt(time.Now().Unix(), 10)
  94. signature, err := signWaffoPancakeRequest(http.MethodPost, waffoPancakeCheckoutPath, timestamp, string(body), privateKey)
  95. if err != nil {
  96. return nil, err
  97. }
  98. req, err := http.NewRequestWithContext(ctx, http.MethodPost, waffoPancakeAuthBaseURL+waffoPancakeCheckoutPath, bytes.NewReader(body))
  99. if err != nil {
  100. return nil, fmt.Errorf("build Waffo Pancake checkout request: %w", err)
  101. }
  102. req.Header.Set("Content-Type", "application/json")
  103. req.Header.Set("X-Merchant-Id", setting.WaffoPancakeMerchantID)
  104. req.Header.Set("X-Timestamp", timestamp)
  105. req.Header.Set("X-Signature", signature)
  106. if setting.WaffoPancakeSandbox {
  107. req.Header.Set("X-Environment", "test")
  108. } else {
  109. req.Header.Set("X-Environment", "prod")
  110. }
  111. resp, err := http.DefaultClient.Do(req)
  112. if err != nil {
  113. return nil, fmt.Errorf("request Waffo Pancake checkout session: %w", err)
  114. }
  115. defer resp.Body.Close()
  116. responseBody, err := io.ReadAll(resp.Body)
  117. if err != nil {
  118. return nil, fmt.Errorf("read Waffo Pancake checkout response: %w", err)
  119. }
  120. var result waffoPancakeCreateSessionResponse
  121. if err := common.Unmarshal(responseBody, &result); err != nil {
  122. return nil, fmt.Errorf("decode Waffo Pancake checkout response: %w", err)
  123. }
  124. if resp.StatusCode >= http.StatusBadRequest {
  125. if len(result.Errors) > 0 {
  126. return nil, fmt.Errorf("Waffo Pancake error (%d): %s", resp.StatusCode, result.Errors[0].Message)
  127. }
  128. return nil, fmt.Errorf("Waffo Pancake checkout request failed with status %d", resp.StatusCode)
  129. }
  130. if len(result.Errors) > 0 {
  131. return nil, fmt.Errorf("Waffo Pancake error: %s", result.Errors[0].Message)
  132. }
  133. if result.Data == nil || result.Data.CheckoutURL == "" || strings.TrimSpace(result.Data.SessionID) == "" {
  134. return nil, fmt.Errorf("Waffo Pancake returned empty checkout session")
  135. }
  136. return result.Data, nil
  137. }
  138. func VerifyConfiguredWaffoPancakeWebhook(payload string, signatureHeader string) (*waffoPancakeWebhookEvent, error) {
  139. environment := resolveWaffoPancakeWebhookEnvironment(payload)
  140. return verifyWaffoPancakeWebhook(payload, signatureHeader, environment)
  141. }
  142. func ResolveWaffoPancakeTradeNo(event *waffoPancakeWebhookEvent) (string, error) {
  143. if event == nil {
  144. return "", fmt.Errorf("missing webhook event")
  145. }
  146. if tradeNo := strings.TrimSpace(event.Data.OrderID); tradeNo != "" {
  147. topUp := model.GetTopUpByTradeNo(tradeNo)
  148. if topUp != nil && topUp.PaymentMethod == model.PaymentMethodWaffoPancake {
  149. return tradeNo, nil
  150. }
  151. return "", fmt.Errorf("waffo pancake order not found for webhook orderId=%s", tradeNo)
  152. }
  153. return "", fmt.Errorf("missing webhook orderId")
  154. }
  155. func normalizeRSAPrivateKey(raw string) (string, error) {
  156. return normalizePEMKey(raw, "PRIVATE KEY", "RSA PRIVATE KEY")
  157. }
  158. func normalizeRSAPublicKey(raw string) (string, error) {
  159. return normalizePEMKey(raw, "PUBLIC KEY", "RSA PUBLIC KEY")
  160. }
  161. func normalizePEMKey(raw string, pkcs8Type string, pkcs1Type string) (string, error) {
  162. if strings.TrimSpace(raw) == "" {
  163. return "", fmt.Errorf("%s is empty", strings.ToLower(pkcs8Type))
  164. }
  165. normalized := strings.TrimSpace(strings.ReplaceAll(raw, `\n`, "\n"))
  166. if strings.Contains(normalized, "BEGIN ") {
  167. block, _ := pem.Decode([]byte(normalized))
  168. if block == nil {
  169. return "", fmt.Errorf("invalid PEM encoded %s", strings.ToLower(pkcs8Type))
  170. }
  171. return string(pem.EncodeToMemory(block)), nil
  172. }
  173. der, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(normalized, "\n", ""))
  174. if err != nil {
  175. return "", fmt.Errorf("invalid base64 encoded %s: %w", strings.ToLower(pkcs8Type), err)
  176. }
  177. pemType := pkcs8Type
  178. if pkcs8Type == "PRIVATE KEY" {
  179. if _, err := x509.ParsePKCS8PrivateKey(der); err != nil {
  180. if _, err := x509.ParsePKCS1PrivateKey(der); err == nil {
  181. pemType = pkcs1Type
  182. } else {
  183. return "", fmt.Errorf("invalid RSA private key")
  184. }
  185. }
  186. } else {
  187. if _, err := x509.ParsePKIXPublicKey(der); err != nil {
  188. if _, err := x509.ParsePKCS1PublicKey(der); err == nil {
  189. pemType = pkcs1Type
  190. } else {
  191. return "", fmt.Errorf("invalid RSA public key")
  192. }
  193. }
  194. }
  195. return string(pem.EncodeToMemory(&pem.Block{Type: pemType, Bytes: der})), nil
  196. }
  197. func signWaffoPancakeRequest(method string, path string, timestamp string, body string, privateKeyPEM string) (string, error) {
  198. block, _ := pem.Decode([]byte(privateKeyPEM))
  199. if block == nil {
  200. return "", fmt.Errorf("invalid RSA private key PEM")
  201. }
  202. var privateKey *rsa.PrivateKey
  203. switch block.Type {
  204. case "PRIVATE KEY":
  205. key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
  206. if err != nil {
  207. return "", fmt.Errorf("parse PKCS#8 private key: %w", err)
  208. }
  209. parsed, ok := key.(*rsa.PrivateKey)
  210. if !ok {
  211. return "", fmt.Errorf("private key is not RSA")
  212. }
  213. privateKey = parsed
  214. case "RSA PRIVATE KEY":
  215. key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
  216. if err != nil {
  217. return "", fmt.Errorf("parse PKCS#1 private key: %w", err)
  218. }
  219. privateKey = key
  220. default:
  221. return "", fmt.Errorf("unsupported private key type: %s", block.Type)
  222. }
  223. canonicalRequest := buildWaffoPancakeCanonicalRequest(method, path, timestamp, body)
  224. digest := sha256.Sum256([]byte(canonicalRequest))
  225. signature, err := rsa.SignPKCS1v15(nil, privateKey, crypto.SHA256, digest[:])
  226. if err != nil {
  227. return "", fmt.Errorf("sign Waffo Pancake request: %w", err)
  228. }
  229. return base64.StdEncoding.EncodeToString(signature), nil
  230. }
  231. func buildWaffoPancakeCanonicalRequest(method string, path string, timestamp string, body string) string {
  232. bodyHash := sha256.Sum256([]byte(body))
  233. return fmt.Sprintf(
  234. "%s\n%s\n%s\n%s",
  235. strings.ToUpper(method),
  236. path,
  237. timestamp,
  238. base64.StdEncoding.EncodeToString(bodyHash[:]),
  239. )
  240. }
  241. func verifyWaffoPancakeWebhook(payload string, signatureHeader string, environment string) (*waffoPancakeWebhookEvent, error) {
  242. if signatureHeader == "" {
  243. return nil, fmt.Errorf("missing X-Waffo-Signature header")
  244. }
  245. timestampPart, signaturePart := parseWaffoPancakeSignatureHeader(signatureHeader)
  246. if timestampPart == "" || signaturePart == "" {
  247. return nil, fmt.Errorf("malformed X-Waffo-Signature header")
  248. }
  249. timestampMs, err := strconv.ParseInt(timestampPart, 10, 64)
  250. if err != nil {
  251. return nil, fmt.Errorf("invalid timestamp in X-Waffo-Signature header")
  252. }
  253. if math.Abs(float64(time.Now().UnixMilli()-timestampMs)) > float64(waffoPancakeDefaultTolerance.Milliseconds()) {
  254. return nil, fmt.Errorf("webhook timestamp outside tolerance window")
  255. }
  256. signatureInput := fmt.Sprintf("%s.%s", timestampPart, payload)
  257. if err := verifyWaffoPancakeWebhookWithKey(signatureInput, signaturePart, resolveWaffoPancakeWebhookPublicKey(environment)); err != nil {
  258. return nil, fmt.Errorf("invalid webhook signature")
  259. }
  260. var event waffoPancakeWebhookEvent
  261. if err := common.Unmarshal([]byte(payload), &event); err != nil {
  262. return nil, fmt.Errorf("parse Waffo Pancake webhook payload: %w", err)
  263. }
  264. return &event, nil
  265. }
  266. func parseWaffoPancakeSignatureHeader(header string) (string, string) {
  267. var timestampPart string
  268. var signaturePart string
  269. for _, pair := range strings.Split(header, ",") {
  270. key, value, found := strings.Cut(strings.TrimSpace(pair), "=")
  271. if !found {
  272. continue
  273. }
  274. switch key {
  275. case "t":
  276. timestampPart = value
  277. case "v1":
  278. signaturePart = value
  279. }
  280. }
  281. return timestampPart, signaturePart
  282. }
  283. func resolveWaffoPancakeWebhookEnvironment(payload string) string {
  284. var envelope struct {
  285. Mode string `json:"mode"`
  286. }
  287. if err := common.Unmarshal([]byte(payload), &envelope); err != nil {
  288. if setting.WaffoPancakeSandbox {
  289. return "test"
  290. }
  291. return "prod"
  292. }
  293. switch strings.ToLower(strings.TrimSpace(envelope.Mode)) {
  294. case "test":
  295. return "test"
  296. case "prod":
  297. return "prod"
  298. default:
  299. if setting.WaffoPancakeSandbox {
  300. return "test"
  301. }
  302. return "prod"
  303. }
  304. }
  305. func resolveWaffoPancakeWebhookPublicKey(environment string) string {
  306. if environment == "prod" {
  307. return strings.TrimSpace(setting.WaffoPancakeWebhookPublicKey)
  308. }
  309. return strings.TrimSpace(setting.WaffoPancakeWebhookTestKey)
  310. }
  311. func verifyWaffoPancakeWebhookWithKey(signatureInput string, signaturePart string, rawPublicKey string) error {
  312. publicKeyPEM, err := normalizeRSAPublicKey(rawPublicKey)
  313. if err != nil {
  314. return err
  315. }
  316. block, _ := pem.Decode([]byte(publicKeyPEM))
  317. if block == nil {
  318. return fmt.Errorf("invalid RSA public key PEM")
  319. }
  320. var publicKey *rsa.PublicKey
  321. switch block.Type {
  322. case "PUBLIC KEY":
  323. key, err := x509.ParsePKIXPublicKey(block.Bytes)
  324. if err != nil {
  325. return fmt.Errorf("parse PKIX public key: %w", err)
  326. }
  327. parsed, ok := key.(*rsa.PublicKey)
  328. if !ok {
  329. return fmt.Errorf("public key is not RSA")
  330. }
  331. publicKey = parsed
  332. case "RSA PUBLIC KEY":
  333. key, err := x509.ParsePKCS1PublicKey(block.Bytes)
  334. if err != nil {
  335. return fmt.Errorf("parse PKCS#1 public key: %w", err)
  336. }
  337. publicKey = key
  338. default:
  339. return fmt.Errorf("unsupported public key type: %s", block.Type)
  340. }
  341. signature, err := base64.StdEncoding.DecodeString(signaturePart)
  342. if err != nil {
  343. return fmt.Errorf("decode webhook signature: %w", err)
  344. }
  345. digest := sha256.Sum256([]byte(signatureInput))
  346. if err := rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, digest[:], signature); err != nil {
  347. return fmt.Errorf("verify webhook signature: %w", err)
  348. }
  349. return nil
  350. }