mirror of
https://github.com/GMWalletApp/epusdt.git
synced 2026-07-07 10:16:15 +00:00
fix(epay): restrict submit type to alipay or supported selectors
This commit is contained in:
@@ -105,9 +105,9 @@ func (c *BaseCommController) SwitchNetwork(ctx echo.Context) (err error) {
|
||||
// the canonical form — the GET variant is identical save the transport.
|
||||
// @Summary Create transaction and redirect (EPAY compat)
|
||||
// @Description Legacy EPAY-style endpoint. Accepts GET (querystring) and POST (form). On success, 302 redirects to /pay/checkout-counter/{trade_id}. Signature uses MD5 of sorted params + secret_key of the api_keys row matching the submitted pid.
|
||||
// @Description After signature verification, token/network resolution is: supported type=token.network selector (for example usdt.tron) first; otherwise request token/network; otherwise epay.default_token / epay.default_network. If token and network are still both empty, the order is created as status=4 placeholder. Supplying only one of token/network remains invalid.
|
||||
// @Description After signature verification, type accepts only either alipay or a supported type=token.network selector (for example usdt.tron). Token/network resolution is: supported selector first; otherwise request token/network; otherwise epay.default_token / epay.default_network. If token and network are still both empty, the order is created as status=4 placeholder. Supplying only one of token/network remains invalid.
|
||||
// @Description Currency resolution is unchanged: request currency -> epay.default_currency -> cny. Supported type selectors bypass only token/network defaults, not currency fallback.
|
||||
// @Description Success return/notify currently use the stored request type when present, and fall back to alipay only when type was missing. The server injects internal payment_type=Epay after EPay signature verification; merchants do not send GMPay payment_type to this endpoint.
|
||||
// @Description Success return/notify reuse the stored request type. On this branch that means either alipay or a supported token.network selector; when the request omitted type, outbound fallback remains alipay. The server injects internal payment_type=Epay after EPay signature verification; merchants do not send GMPay payment_type to this endpoint.
|
||||
// @Tags Payment
|
||||
// @Accept x-www-form-urlencoded
|
||||
// @Produce html
|
||||
@@ -117,7 +117,7 @@ func (c *BaseCommController) SwitchNetwork(ctx echo.Context) (err error) {
|
||||
// @Param notify_url query string false "Callback URL (GET query)"
|
||||
// @Param return_url query string false "Redirect URL after payment (GET query)"
|
||||
// @Param name query string false "Order name (GET query)"
|
||||
// @Param type query string false "Opaque merchant type or supported token.network selector such as alipay or usdt.tron (GET query)"
|
||||
// @Param type query string false "Either alipay or a supported token.network selector such as usdt.tron (GET query)"
|
||||
// @Param sign query string false "MD5 signature (GET query)"
|
||||
// @Param sign_type query string false "Signature type (MD5, GET query)"
|
||||
// @Param pid formData integer true "API key PID"
|
||||
@@ -126,7 +126,7 @@ func (c *BaseCommController) SwitchNetwork(ctx echo.Context) (err error) {
|
||||
// @Param notify_url formData string true "Callback URL"
|
||||
// @Param return_url formData string false "Redirect URL after payment"
|
||||
// @Param name formData string false "Order name"
|
||||
// @Param type formData string false "Opaque merchant type or supported token.network selector such as alipay or usdt.tron"
|
||||
// @Param type formData string false "Either alipay or a supported token.network selector such as usdt.tron"
|
||||
// @Param sign formData string true "MD5 signature"
|
||||
// @Param sign_type formData string false "Signature type (MD5)"
|
||||
// @Success 302 "Redirect to checkout counter"
|
||||
|
||||
@@ -36,7 +36,7 @@ func (c *BaseCommController) CheckoutCounter(ctx echo.Context) (err error) {
|
||||
// Non-EPay or not-yet-paid orders are sent back to the checkout counter.
|
||||
// @Summary Return to merchant (EPAY compat)
|
||||
// @Description Browser-facing success return hop for EPay orders. Paid EPay orders are redirected to the merchant return_url with signed legacy EPay query params. Orders that are not EPay or not yet paid are redirected back to the checkout counter.
|
||||
// @Description The signed query params currently use the stored request type when present; otherwise type=alipay is returned for compatibility.
|
||||
// @Description The signed query params reuse the stored request type. On this branch that means either alipay or a supported token.network selector; if the original request omitted type, type=alipay is returned for compatibility.
|
||||
// @Description This route also returns explicit business errors when the merchant return_url is missing, the order API key is unavailable, or EPay signature construction fails.
|
||||
// @Tags Payment
|
||||
// @Produce html
|
||||
|
||||
+52
-66
@@ -5,7 +5,6 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
@@ -631,7 +630,7 @@ func TestSendOrderCallbackEpayUsesApiKeySecretByPid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendOrderCallbackEpayPreservesStoredTypeValues(t *testing.T) {
|
||||
func TestSendOrderCallbackEpayPreservesStoredAlipayType(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -645,73 +644,60 @@ func TestSendOrderCallbackEpayPreservesStoredTypeValues(t *testing.T) {
|
||||
t.Fatalf("create epay api key: %v", err)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
epayType string
|
||||
}{
|
||||
{name: "alipay", epayType: "alipay"},
|
||||
{name: "non_selector", epayType: "usdt-tron"},
|
||||
{name: "unsupported_selector", epayType: "usdc.tron"},
|
||||
formPayload := map[string]string{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
for k, v := range r.Form {
|
||||
if len(v) > 0 {
|
||||
formPayload[k] = v[0]
|
||||
}
|
||||
}
|
||||
_, _ = io.WriteString(w, "ok")
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
order := &mdb.Orders{
|
||||
TradeId: "trade_epay_type_case_alipay",
|
||||
OrderId: "order_epay_type_case_alipay",
|
||||
Amount: 1,
|
||||
Currency: "CNY",
|
||||
ActualAmount: 1,
|
||||
ReceiveAddress: "wallet_epay_type_case",
|
||||
Token: "USDT",
|
||||
Name: "VIP",
|
||||
Status: mdb.StatusPaySuccess,
|
||||
NotifyUrl: server.URL,
|
||||
BlockTransactionId: "block_epay_type_case_alipay",
|
||||
EpayType: "alipay",
|
||||
PaymentType: "epay",
|
||||
ApiKeyID: key.ID,
|
||||
}
|
||||
|
||||
for idx, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
formPayload := map[string]string{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
for k, v := range r.Form {
|
||||
if len(v) > 0 {
|
||||
formPayload[k] = v[0]
|
||||
}
|
||||
}
|
||||
_, _ = io.WriteString(w, "ok")
|
||||
}))
|
||||
defer server.Close()
|
||||
if err := sendOrderCallback(order); err != nil {
|
||||
t.Fatalf("send epay callback: %v", err)
|
||||
}
|
||||
if got := formPayload["type"]; got != "alipay" {
|
||||
t.Fatalf("type = %q, want alipay", got)
|
||||
}
|
||||
|
||||
order := &mdb.Orders{
|
||||
TradeId: "trade_epay_type_case_" + strconv.Itoa(idx),
|
||||
OrderId: "order_epay_type_case_" + strconv.Itoa(idx),
|
||||
Amount: 1,
|
||||
Currency: "CNY",
|
||||
ActualAmount: 1,
|
||||
ReceiveAddress: "wallet_epay_type_case",
|
||||
Token: "USDT",
|
||||
Name: "VIP",
|
||||
Status: mdb.StatusPaySuccess,
|
||||
NotifyUrl: server.URL,
|
||||
BlockTransactionId: "block_epay_type_case_" + strconv.Itoa(idx),
|
||||
EpayType: tc.epayType,
|
||||
PaymentType: "epay",
|
||||
ApiKeyID: key.ID,
|
||||
}
|
||||
|
||||
if err := sendOrderCallback(order); err != nil {
|
||||
t.Fatalf("send epay callback: %v", err)
|
||||
}
|
||||
if got := formPayload["type"]; got != tc.epayType {
|
||||
t.Fatalf("type = %q, want %q", got, tc.epayType)
|
||||
}
|
||||
|
||||
signParams := map[string]interface{}{
|
||||
"pid": formPayload["pid"],
|
||||
"trade_no": formPayload["trade_no"],
|
||||
"out_trade_no": formPayload["out_trade_no"],
|
||||
"type": formPayload["type"],
|
||||
"name": formPayload["name"],
|
||||
"money": formPayload["money"],
|
||||
"trade_status": formPayload["trade_status"],
|
||||
}
|
||||
calcSig, err := sign.Get(signParams, key.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatalf("calc epay signature: %v", err)
|
||||
}
|
||||
if got := formPayload["sign"]; got != calcSig {
|
||||
t.Fatalf("sign = %q, want %q", got, calcSig)
|
||||
}
|
||||
})
|
||||
signParams := map[string]interface{}{
|
||||
"pid": formPayload["pid"],
|
||||
"trade_no": formPayload["trade_no"],
|
||||
"out_trade_no": formPayload["out_trade_no"],
|
||||
"type": formPayload["type"],
|
||||
"name": formPayload["name"],
|
||||
"money": formPayload["money"],
|
||||
"trade_status": formPayload["trade_status"],
|
||||
}
|
||||
calcSig, err := sign.Get(signParams, key.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatalf("calc epay signature: %v", err)
|
||||
}
|
||||
if got := formPayload["sign"]; got != calcSig {
|
||||
t.Fatalf("sign = %q, want %q", got, calcSig)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -23,8 +23,9 @@ import (
|
||||
)
|
||||
|
||||
// resolveEPayTypeSelector only claims the type as a selector when it maps to a
|
||||
// currently supported payment asset; otherwise the caller should keep the
|
||||
// legacy token/network/default resolution and treat type as opaque.
|
||||
// currently supported payment asset. Non-selector values are handled by the
|
||||
// caller, which now only accepts empty type or alipay before falling back to
|
||||
// request token/network/default resolution.
|
||||
func resolveEPayTypeSelector(rawType string) (string, string, bool, error) {
|
||||
rawType = strings.TrimSpace(rawType)
|
||||
if rawType == "" || strings.Count(rawType, ".") != 1 {
|
||||
@@ -168,6 +169,9 @@ func RegisterRoute(e *echo.Echo) {
|
||||
if err != nil {
|
||||
return comm.Ctrl.FailJson(ctx, constant.SystemErr)
|
||||
}
|
||||
if epayType != "" && !selectorMatched && !strings.EqualFold(epayType, "alipay") {
|
||||
return comm.Ctrl.FailJson(ctx, constant.ParamsMarshalErr)
|
||||
}
|
||||
token := strings.TrimSpace(getString(params, "token"))
|
||||
network := strings.TrimSpace(getString(params, "network"))
|
||||
if selectorMatched {
|
||||
|
||||
+51
-86
@@ -1471,7 +1471,46 @@ func TestEpaySubmitPhpWithoutTokenNetworkDefaultsCreatesPlaceholder(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestEpaySubmitPhpNonSelectorTypeKeepsOldLogic(t *testing.T) {
|
||||
func TestEpaySubmitPhpWithoutTypeKeepsCurrentBehavior(t *testing.T) {
|
||||
e := setupTestEnv(t)
|
||||
|
||||
if err := data.SetSetting(mdb.SettingGroupEpay, mdb.SettingKeyEpayDefaultToken, "usdt", mdb.SettingTypeString); err != nil {
|
||||
t.Fatalf("seed epay.default_token: %v", err)
|
||||
}
|
||||
if err := data.SetSetting(mdb.SettingGroupEpay, mdb.SettingKeyEpayDefaultNetwork, "tron", mdb.SettingTypeString); err != nil {
|
||||
t.Fatalf("seed epay.default_network: %v", err)
|
||||
}
|
||||
|
||||
values := signEpayValues(url.Values{
|
||||
"pid": {"1"},
|
||||
"name": {"epay-empty-type-001"},
|
||||
"money": {"1.00"},
|
||||
"out_trade_no": {"epay-empty-type-001"},
|
||||
"notify_url": {"https://93.184.216.34/notify"},
|
||||
"return_url": {"http://localhost/return"},
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/payments/epay/v1/order/create-transaction/submit.php?"+values.Encode(), nil)
|
||||
rec := httptest.NewRecorder()
|
||||
e.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusFound {
|
||||
t.Fatalf("expected 302, got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
tradeID := strings.TrimPrefix(rec.Header().Get("Location"), "/pay/checkout-counter/")
|
||||
order, err := data.GetOrderInfoByTradeId(tradeID)
|
||||
if err != nil {
|
||||
t.Fatalf("reload epay empty-type order: %v", err)
|
||||
}
|
||||
if order.Token != "USDT" || order.Network != mdb.NetworkTron || order.ReceiveAddress != "TTestTronAddress001" {
|
||||
t.Fatalf("empty-type order fields = token %q network %q address %q", order.Token, order.Network, order.ReceiveAddress)
|
||||
}
|
||||
if order.EpayType != "" {
|
||||
t.Fatalf("epay_type = %q, want empty", order.EpayType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEpaySubmitPhpRejectsNonSelectorType(t *testing.T) {
|
||||
e := setupTestEnv(t)
|
||||
|
||||
if err := data.SetSetting(mdb.SettingGroupEpay, mdb.SettingKeyEpayDefaultToken, "usdt", mdb.SettingTypeString); err != nil {
|
||||
@@ -1495,23 +1534,16 @@ func TestEpaySubmitPhpNonSelectorTypeKeepsOldLogic(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
e.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusFound {
|
||||
t.Fatalf("expected 302, got %d body=%s", rec.Code, rec.Body.String())
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
tradeID := strings.TrimPrefix(rec.Header().Get("Location"), "/pay/checkout-counter/")
|
||||
order, err := data.GetOrderInfoByTradeId(tradeID)
|
||||
if err != nil {
|
||||
t.Fatalf("reload epay non-selector order: %v", err)
|
||||
}
|
||||
if order.Token != "USDT" || order.Network != mdb.NetworkTron || order.ReceiveAddress != "TTestTronAddress001" {
|
||||
t.Fatalf("non-selector order fields = token %q network %q address %q", order.Token, order.Network, order.ReceiveAddress)
|
||||
}
|
||||
if order.EpayType != "usdt-tron" {
|
||||
t.Fatalf("epay_type = %q, want usdt-tron", order.EpayType)
|
||||
resp := parseResp(t, rec)
|
||||
if got := int(resp["status_code"].(float64)); got != 10009 {
|
||||
t.Fatalf("status_code = %d, want 10009", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEpaySubmitPhpUnsupportedSelectorFallsBackToOldLogic(t *testing.T) {
|
||||
func TestEpaySubmitPhpRejectsUnsupportedSelectorType(t *testing.T) {
|
||||
e := setupTestEnv(t)
|
||||
|
||||
if err := data.SetSetting(mdb.SettingGroupEpay, mdb.SettingKeyEpayDefaultToken, "usdt", mdb.SettingTypeString); err != nil {
|
||||
@@ -1535,19 +1567,12 @@ func TestEpaySubmitPhpUnsupportedSelectorFallsBackToOldLogic(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
e.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusFound {
|
||||
t.Fatalf("expected 302, got %d body=%s", rec.Code, rec.Body.String())
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
tradeID := strings.TrimPrefix(rec.Header().Get("Location"), "/pay/checkout-counter/")
|
||||
order, err := data.GetOrderInfoByTradeId(tradeID)
|
||||
if err != nil {
|
||||
t.Fatalf("reload epay unsupported selector order: %v", err)
|
||||
}
|
||||
if order.Token != "USDT" || order.Network != mdb.NetworkTron || order.ReceiveAddress != "TTestTronAddress001" {
|
||||
t.Fatalf("unsupported selector order fields = token %q network %q address %q", order.Token, order.Network, order.ReceiveAddress)
|
||||
}
|
||||
if order.EpayType != "usdc.tron" {
|
||||
t.Fatalf("epay_type = %q, want usdc.tron", order.EpayType)
|
||||
resp := parseResp(t, rec)
|
||||
if got := int(resp["status_code"].(float64)); got != 10009 {
|
||||
t.Fatalf("status_code = %d, want 10009", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1897,66 +1922,6 @@ func TestPayReturn_PaidEpayRedirectsWithOriginalType(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPayReturn_PaidEpayRedirectsWithNonSelectorType(t *testing.T) {
|
||||
e := setupTestEnv(t)
|
||||
tradeID := mustCreateEPayOrder(t, e, "epay-return-non-selector-001", "https://merchant.example/return", url.Values{
|
||||
"type": {"usdt-tron"},
|
||||
"token": {"usdt"},
|
||||
"network": {"tron"},
|
||||
"currency": {"cny"},
|
||||
})
|
||||
if err := dao.Mdb.Model(&mdb.Orders{}).
|
||||
Where("trade_id = ?", tradeID).
|
||||
Update("status", mdb.StatusPaySuccess).Error; err != nil {
|
||||
t.Fatalf("mark order paid: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/pay/return/"+tradeID, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
e.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusFound {
|
||||
t.Fatalf("expected 302, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
targetURL, err := url.Parse(rec.Header().Get("Location"))
|
||||
if err != nil {
|
||||
t.Fatalf("parse redirect location: %v", err)
|
||||
}
|
||||
if got := targetURL.Query().Get("type"); got != "usdt-tron" {
|
||||
t.Fatalf("type = %q, want usdt-tron", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPayReturn_PaidEpayRedirectsWithUnsupportedSelectorType(t *testing.T) {
|
||||
e := setupTestEnv(t)
|
||||
tradeID := mustCreateEPayOrder(t, e, "epay-return-unsel-001", "https://merchant.example/return", url.Values{
|
||||
"type": {"usdc.tron"},
|
||||
"token": {"usdt"},
|
||||
"network": {"tron"},
|
||||
"currency": {"cny"},
|
||||
})
|
||||
if err := dao.Mdb.Model(&mdb.Orders{}).
|
||||
Where("trade_id = ?", tradeID).
|
||||
Update("status", mdb.StatusPaySuccess).Error; err != nil {
|
||||
t.Fatalf("mark order paid: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/pay/return/"+tradeID, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
e.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusFound {
|
||||
t.Fatalf("expected 302, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
targetURL, err := url.Parse(rec.Header().Get("Location"))
|
||||
if err != nil {
|
||||
t.Fatalf("parse redirect location: %v", err)
|
||||
}
|
||||
if got := targetURL.Query().Get("type"); got != "usdc.tron" {
|
||||
t.Fatalf("type = %q, want usdc.tron", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPayReturn_UnpaidEpayRedirectsBackToCheckout(t *testing.T) {
|
||||
e := setupTestEnv(t)
|
||||
tradeID := mustCreateEPayOrder(t, e, "epay-return-unpaid-001", "https://merchant.example/return", url.Values{
|
||||
|
||||
Reference in New Issue
Block a user