Subversion Repositories SmartDukaan

Rev

Rev 36510 | Blame | Compare with Previous | Last modification | View Log | RSS feed

package com.spice.profitmandi.web.interceptor;

import com.spice.profitmandi.web.filter.RequestCachingFilter;
import com.spice.profitmandi.web.model.LoginDetails;
import com.spice.profitmandi.web.util.CookiesProcessor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.security.MessageDigest;
import java.util.concurrent.TimeUnit;

@Component
public class PostInterceptor implements HandlerInterceptor {

    private static final Logger LOGGER = LogManager.getLogger(PostInterceptor.class);
    private static final String IDEM_PREFIX = "idem:";
    private static final long IDEM_TTL_SECONDS = 300;
    private static final String REQUEST_ATTR_IDEM_KEY = "postInterceptor.idemKey";

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private CookiesProcessor cookiesProcessor;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // Idempotency applies only to state-changing methods. GET/HEAD/OPTIONS are
        // safe/repeatable and must never be deduped (a stale read must not be 400'd).
        if (!isMutatingMethod(request.getMethod())) {
            return true;
        }

        String idemKey = buildIdempotencyKey(request);
        if (idemKey == null) {
            // Nothing reliable to key on (e.g. multipart upload with no client key).
            return true;
        }

        String redisKey = IDEM_PREFIX + idemKey;
        Boolean claimed = redisTemplate.opsForValue()
                .setIfAbsent(redisKey, "pending", IDEM_TTL_SECONDS, TimeUnit.SECONDS);

        if (Boolean.TRUE.equals(claimed)) {
            request.setAttribute(REQUEST_ATTR_IDEM_KEY, idemKey);
            return true;
        }

        LOGGER.info("Duplicate request detected: idemKey={}, uri={}", idemKey, request.getRequestURI());
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        response.getWriter().write("Duplicate request.");
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
            throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        String idemKey = (String) request.getAttribute(REQUEST_ATTR_IDEM_KEY);
        if (idemKey == null) {
            return;
        }
        String redisKey = IDEM_PREFIX + idemKey;
        if (ex == null && response.getStatus() >= 200 && response.getStatus() < 300) {
            redisTemplate.opsForValue().set(redisKey, "done", IDEM_TTL_SECONDS, TimeUnit.SECONDS);
        } else {
            redisTemplate.delete(redisKey);
        }
    }

    private boolean isMutatingMethod(String method) {
        return "POST".equalsIgnoreCase(method)
                || "PUT".equalsIgnoreCase(method)
                || "PATCH".equalsIgnoreCase(method)
                || "DELETE".equalsIgnoreCase(method);
    }

    /**
     * Server-authoritative idempotency key. Dedupes on the request CONTENT
     * (a hash of the body) scoped to the authenticated partner + method + uri, so
     * that a client which rotates or otherwise mismanages its IdempotencyKey
     * header (the jQuery SPA, or an old mobile build that cannot be force-updated)
     * can no longer produce distinct keys for identical submissions. The client
     * header is used only as a fallback when the body is not cached (multipart /
     * form-encoded requests).
     */
    private String buildIdempotencyKey(HttpServletRequest request) {
        String base = null;
        if (request instanceof RequestCachingFilter.CachedBodyRequest) {
            byte[] body = ((RequestCachingFilter.CachedBodyRequest) request).getCachedBody();
            if (body != null && body.length > 0) {
                base = "body:" + sha256(body);
            }
        }
        if (base == null) {
            String header = request.getHeader("IdempotencyKey");
            if (header != null && !header.isEmpty()) {
                base = "hdr:" + header;
            }
        }
        if (base == null) {
            return null;
        }

        String principal = resolvePrincipal(request);
        String query = request.getQueryString();
        String scope = principal + "|" + request.getMethod() + "|" + request.getRequestURI()
                + (query != null ? "?" + query : "") + "|" + base;
        return sha256(scope.getBytes(java.nio.charset.StandardCharsets.UTF_8));
    }

    /**
     * Scope the key to the logged-in partner so two different partners submitting
     * an identical payload are not collapsed into a single claim. Falls back to
     * "anon" for unauthenticated / public mutating endpoints.
     */
    private String resolvePrincipal(HttpServletRequest request) {
        try {
            LoginDetails login = cookiesProcessor.getCookiesObject(request);
            if (login != null && login.getFofoId() > 0) {
                return "fofo:" + login.getFofoId();
            }
        } catch (Exception ignore) {
            // session/cookies absent on public endpoints -> fall through to anon
        }
        return "anon";
    }

    private static String sha256(byte[] data) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(data);
            StringBuilder hex = new StringBuilder(64);
            for (byte b : hash) {
                hex.append(String.format("%02x", b));
            }
            return hex.toString();
        } catch (Exception e) {
            throw new RuntimeException("SHA-256 not available", e);
        }
    }
}