fix(api): bound claim wire fields end to end
This commit is contained in:
@@ -10,12 +10,12 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"math/big"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"cmbuyer/admin/internal/deviceauth"
|
||||
taskmodel "cmbuyer/admin/internal/tasks"
|
||||
)
|
||||
|
||||
const writeTimeout = 2 * time.Second
|
||||
@@ -80,7 +80,7 @@ func (store *Store) validateStoredClaims(ctx context.Context) error {
|
||||
rows, err := store.database.QueryContext(ctx, `SELECT claims.claimed_by_device_id, claims.task_id, claims.authorization_id,
|
||||
claims.attempt_id, claims.claim_generation, claims.claim_nonce, typeof(claims.claim_nonce), length(claims.claim_nonce),
|
||||
claims.claim_token_sha256, typeof(claims.claim_token_sha256), length(claims.claim_token_sha256),
|
||||
claims.authorization_task_version, claims.goods_id, claims.sku_color, claims.sku_size,
|
||||
claims.task_title, claims.authorization_task_version, claims.goods_id, claims.sku_color, claims.sku_size,
|
||||
claims.quantity, claims.total_price_cap, claims.authorization_expires_at, claims.closed_at,
|
||||
attempts.claim_generation, attempts.status, authorizations.status, tasks.status
|
||||
FROM purchase_attempt_claims AS claims
|
||||
@@ -96,21 +96,21 @@ func (store *Store) validateStoredClaims(ctx context.Context) error {
|
||||
var deviceID, taskID, authorizationID, attemptID string
|
||||
var generation, authorizationTaskVersion, quantity int
|
||||
var nonce, storedHash []byte
|
||||
var nonceType, hashType, goodsID, color, size, price, expires string
|
||||
var nonceType, hashType, title, goodsID, color, size, price, expires string
|
||||
var nonceLength, hashLength int
|
||||
var closed, attemptStatus, authorizationStatus, taskStatus sql.NullString
|
||||
var attemptGeneration sql.NullInt64
|
||||
if err := rows.Scan(&deviceID, &taskID, &authorizationID, &attemptID, &generation,
|
||||
&nonce, &nonceType, &nonceLength, &storedHash, &hashType, &hashLength,
|
||||
&authorizationTaskVersion, &goodsID, &color, &size, &quantity, &price, &expires, &closed,
|
||||
&title, &authorizationTaskVersion, &goodsID, &color, &size, &quantity, &price, &expires, &closed,
|
||||
&attemptGeneration, &attemptStatus, &authorizationStatus, &taskStatus); err != nil {
|
||||
return errors.New("validate stored task claims")
|
||||
}
|
||||
if !deviceauth.ValidDeviceID(deviceID) || !validUUID(taskID) || !validUUID(authorizationID) || !validUUID(attemptID) ||
|
||||
generation <= 0 || nonceType != "blob" || nonceLength != sha256.Size || len(nonce) != sha256.Size ||
|
||||
hashType != "blob" || hashLength != sha256.Size || len(storedHash) != sha256.Size ||
|
||||
authorizationTaskVersion <= 0 || !digitsOnly(goodsID) || color == "" || size == "" || quantity <= 0 ||
|
||||
!canonicalMoney(price) || !validCanonicalTime(expires) || (closed.Valid && !validCanonicalTime(closed.String)) ||
|
||||
authorizationTaskVersion <= 0 || !taskmodel.ValidTaskWireFields(title, goodsID, color, size, price) || quantity <= 0 ||
|
||||
!validCanonicalTime(expires) || (closed.Valid && !validCanonicalTime(closed.String)) ||
|
||||
!attemptGeneration.Valid || attemptGeneration.Int64 != int64(generation) ||
|
||||
!validAttemptStatus(attemptStatus) || !validAuthorizationStatus(authorizationStatus) || !validTaskStatus(taskStatus) {
|
||||
return errors.New("stored task claim metadata is invalid")
|
||||
@@ -577,9 +577,9 @@ func (store *Store) scanClaim(row rowScanner) (claimRecord, bool, error) {
|
||||
if !validUUID(record.AttemptID) || !validUUID(record.TaskID) || !validUUID(record.AuthorizationID) ||
|
||||
!deviceauth.ValidDeviceID(record.DeviceID) || !validUUID(record.SessionID) || record.Generation <= 0 ||
|
||||
record.CurrentAttemptGeneration != record.Generation ||
|
||||
record.TaskVersion <= 0 || record.AuthorizationTaskVersion <= 0 || strings.TrimSpace(record.TaskTitle) == "" ||
|
||||
!digitsOnly(record.GoodsID) || record.SKUColor == "" || record.SKUSize == "" || record.Quantity <= 0 ||
|
||||
!canonicalMoney(record.TotalPriceCap) || len(record.Nonce) != sha256.Size || len(record.TokenHash) != sha256.Size {
|
||||
record.TaskVersion <= 0 || record.AuthorizationTaskVersion <= 0 ||
|
||||
!taskmodel.ValidTaskWireFields(record.TaskTitle, record.GoodsID, record.SKUColor, record.SKUSize, record.TotalPriceCap) ||
|
||||
record.Quantity <= 0 || len(record.Nonce) != sha256.Size || len(record.TokenHash) != sha256.Size {
|
||||
return claimRecord{}, false, errors.New("stored task claim metadata is invalid")
|
||||
}
|
||||
record.LeaseExpiresAt, err = parseCanonicalTime(record.LeaseExpiresText)
|
||||
@@ -602,7 +602,11 @@ func (store *Store) scanClaim(row rowScanner) (claimRecord, bool, error) {
|
||||
}
|
||||
|
||||
func (record claimRecord) authorizationConsistent() bool {
|
||||
return record.AuthorizationTaskVersion == record.CurrentAuthorizationTaskVersion &&
|
||||
return taskmodel.ValidAuthorizationFields(record.CurrentGoodsID, record.CurrentSKUColor,
|
||||
record.CurrentSKUSize, record.CurrentTotalPriceCap) &&
|
||||
taskmodel.ValidTaskWireFields(record.CurrentTaskTitle, record.CurrentTaskGoodsID,
|
||||
record.CurrentTaskSKUColor, record.CurrentTaskSKUSize, record.CurrentTaskMaxTotalPrice) &&
|
||||
record.AuthorizationTaskVersion == record.CurrentAuthorizationTaskVersion &&
|
||||
record.GoodsID == record.CurrentGoodsID && record.SKUColor == record.CurrentSKUColor &&
|
||||
record.SKUSize == record.CurrentSKUSize && record.Quantity == record.CurrentQuantity &&
|
||||
record.TotalPriceCap == record.CurrentTotalPriceCap &&
|
||||
@@ -623,8 +627,12 @@ func (record claimRecord) recoverableBusinessState() bool {
|
||||
}
|
||||
|
||||
func (store *Store) responseFor(record claimRecord, responseLease string) (ClaimResponse, error) {
|
||||
if !validCanonicalTime(responseLease) {
|
||||
return ClaimResponse{}, errors.New("stored claim response lease is invalid")
|
||||
// Exact idempotent replay is allowed to ignore later source-row drift, but the
|
||||
// immutable response snapshot itself must still satisfy the current wire bounds.
|
||||
if !validCanonicalTime(responseLease) ||
|
||||
!taskmodel.ValidTaskWireFields(record.TaskTitle, record.GoodsID, record.SKUColor, record.SKUSize, record.TotalPriceCap) ||
|
||||
record.Quantity <= 0 {
|
||||
return ClaimResponse{}, errors.New("stored claim response snapshot is invalid")
|
||||
}
|
||||
token := deriveToken(store.secret, record.DeviceID, record.TaskID, record.AuthorizationID, record.AttemptID, record.Generation, record.Nonce)
|
||||
return ClaimResponse{
|
||||
@@ -690,8 +698,8 @@ func findCandidate(ctx context.Context, transaction *sql.Tx, now time.Time) (can
|
||||
|
||||
func validCandidate(item candidate) bool {
|
||||
return validUUID(item.AuthorizationID) && validUUID(item.TaskID) && item.TaskVersion > 0 && item.TaskVersion < math.MaxInt &&
|
||||
strings.TrimSpace(item.Title) != "" && digitsOnly(item.GoodsID) && item.SKUColor != "" && item.SKUSize != "" &&
|
||||
item.Quantity > 0 && canonicalMoney(item.TotalPriceCap)
|
||||
taskmodel.ValidTaskWireFields(item.Title, item.GoodsID, item.SKUColor, item.SKUSize, item.TotalPriceCap) &&
|
||||
item.Quantity > 0
|
||||
}
|
||||
|
||||
func validAttemptStatus(value sql.NullString) bool {
|
||||
@@ -798,33 +806,6 @@ func validUUID(value string) bool {
|
||||
return value[14] == '4' && (value[19] == '8' || value[19] == '9' || value[19] == 'a' || value[19] == 'b')
|
||||
}
|
||||
|
||||
func digitsOnly(value string) bool {
|
||||
if value == "" {
|
||||
return false
|
||||
}
|
||||
for _, character := range value {
|
||||
if character < '0' || character > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func canonicalMoney(value string) bool {
|
||||
parts := strings.Split(value, ".")
|
||||
if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) != 2 || (len(parts[0]) > 1 && parts[0][0] == '0') {
|
||||
return false
|
||||
}
|
||||
for _, part := range parts {
|
||||
if !digitsOnly(part) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
cents := new(big.Int)
|
||||
_, ok := cents.SetString(parts[0]+parts[1], 10)
|
||||
return ok && cents.Sign() > 0
|
||||
}
|
||||
|
||||
func productURL(goodsID string) string {
|
||||
return "https://mobile.yangkeduo.com/goods.html?goods_id=" + goodsID
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user