Subversion Repositories SmartDukaan

Rev

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

Rev Author Line No. Line
33756 ranu 1
package com.spice.profitmandi.web.interceptor;
2
 
36510 amit 3
import com.spice.profitmandi.web.filter.RequestCachingFilter;
36819 amit 4
import com.spice.profitmandi.web.model.LoginDetails;
5
import com.spice.profitmandi.web.util.CookiesProcessor;
33756 ranu 6
import org.apache.logging.log4j.LogManager;
7
import org.apache.logging.log4j.Logger;
8
import org.springframework.beans.factory.annotation.Autowired;
36510 amit 9
import org.springframework.data.redis.core.RedisTemplate;
33756 ranu 10
import org.springframework.stereotype.Component;
11
import org.springframework.web.servlet.HandlerInterceptor;
12
import org.springframework.web.servlet.ModelAndView;
13
 
14
import javax.servlet.http.HttpServletRequest;
15
import javax.servlet.http.HttpServletResponse;
36510 amit 16
import java.security.MessageDigest;
17
import java.util.concurrent.TimeUnit;
33756 ranu 18
 
19
@Component
20
public class PostInterceptor implements HandlerInterceptor {
21
 
36510 amit 22
    private static final Logger LOGGER = LogManager.getLogger(PostInterceptor.class);
23
    private static final String IDEM_PREFIX = "idem:";
24
    private static final long IDEM_TTL_SECONDS = 300;
25
    private static final String REQUEST_ATTR_IDEM_KEY = "postInterceptor.idemKey";
26
 
33756 ranu 27
    @Autowired
36510 amit 28
    private RedisTemplate<String, Object> redisTemplate;
33756 ranu 29
 
36819 amit 30
    @Autowired
31
    private CookiesProcessor cookiesProcessor;
32
 
33756 ranu 33
    @Override
36510 amit 34
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
36819 amit 35
        // Idempotency applies only to state-changing methods. GET/HEAD/OPTIONS are
36
        // safe/repeatable and must never be deduped (a stale read must not be 400'd).
37
        if (!isMutatingMethod(request.getMethod())) {
36510 amit 38
            return true;
39
        }
40
 
36819 amit 41
        String idemKey = buildIdempotencyKey(request);
36510 amit 42
        if (idemKey == null) {
36819 amit 43
            // Nothing reliable to key on (e.g. multipart upload with no client key).
36510 amit 44
            return true;
45
        }
46
 
47
        String redisKey = IDEM_PREFIX + idemKey;
48
        Boolean claimed = redisTemplate.opsForValue()
49
                .setIfAbsent(redisKey, "pending", IDEM_TTL_SECONDS, TimeUnit.SECONDS);
50
 
51
        if (Boolean.TRUE.equals(claimed)) {
52
            request.setAttribute(REQUEST_ATTR_IDEM_KEY, idemKey);
53
            return true;
54
        }
55
 
56
        LOGGER.info("Duplicate request detected: idemKey={}, uri={}", idemKey, request.getRequestURI());
57
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
58
        response.getWriter().write("Duplicate request.");
59
        return false;
60
    }
61
 
62
    @Override
63
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
33756 ranu 64
            throws Exception {
65
    }
66
 
67
    @Override
36510 amit 68
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
33756 ranu 69
            throws Exception {
36510 amit 70
        String idemKey = (String) request.getAttribute(REQUEST_ATTR_IDEM_KEY);
71
        if (idemKey == null) {
72
            return;
73
        }
74
        String redisKey = IDEM_PREFIX + idemKey;
75
        if (ex == null && response.getStatus() >= 200 && response.getStatus() < 300) {
76
            redisTemplate.opsForValue().set(redisKey, "done", IDEM_TTL_SECONDS, TimeUnit.SECONDS);
77
        } else {
78
            redisTemplate.delete(redisKey);
79
        }
33756 ranu 80
    }
81
 
36819 amit 82
    private boolean isMutatingMethod(String method) {
83
        return "POST".equalsIgnoreCase(method)
84
                || "PUT".equalsIgnoreCase(method)
85
                || "PATCH".equalsIgnoreCase(method)
86
                || "DELETE".equalsIgnoreCase(method);
87
    }
88
 
89
    /**
90
     * Server-authoritative idempotency key. Dedupes on the request CONTENT
91
     * (a hash of the body) scoped to the authenticated partner + method + uri, so
92
     * that a client which rotates or otherwise mismanages its IdempotencyKey
93
     * header (the jQuery SPA, or an old mobile build that cannot be force-updated)
94
     * can no longer produce distinct keys for identical submissions. The client
95
     * header is used only as a fallback when the body is not cached (multipart /
96
     * form-encoded requests).
97
     */
98
    private String buildIdempotencyKey(HttpServletRequest request) {
99
        String base = null;
100
        if (request instanceof RequestCachingFilter.CachedBodyRequest) {
36510 amit 101
            byte[] body = ((RequestCachingFilter.CachedBodyRequest) request).getCachedBody();
36819 amit 102
            if (body != null && body.length > 0) {
103
                base = "body:" + sha256(body);
33756 ranu 104
            }
36510 amit 105
        }
36819 amit 106
        if (base == null) {
107
            String header = request.getHeader("IdempotencyKey");
108
            if (header != null && !header.isEmpty()) {
109
                base = "hdr:" + header;
110
            }
111
        }
112
        if (base == null) {
113
            return null;
114
        }
115
 
116
        String principal = resolvePrincipal(request);
117
        String query = request.getQueryString();
118
        String scope = principal + "|" + request.getMethod() + "|" + request.getRequestURI()
119
                + (query != null ? "?" + query : "") + "|" + base;
120
        return sha256(scope.getBytes(java.nio.charset.StandardCharsets.UTF_8));
36510 amit 121
    }
33756 ranu 122
 
36819 amit 123
    /**
124
     * Scope the key to the logged-in partner so two different partners submitting
125
     * an identical payload are not collapsed into a single claim. Falls back to
126
     * "anon" for unauthenticated / public mutating endpoints.
127
     */
128
    private String resolvePrincipal(HttpServletRequest request) {
129
        try {
130
            LoginDetails login = cookiesProcessor.getCookiesObject(request);
131
            if (login != null && login.getFofoId() > 0) {
132
                return "fofo:" + login.getFofoId();
133
            }
134
        } catch (Exception ignore) {
135
            // session/cookies absent on public endpoints -> fall through to anon
136
        }
137
        return "anon";
138
    }
139
 
36510 amit 140
    private static String sha256(byte[] data) {
141
        try {
142
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
143
            byte[] hash = digest.digest(data);
144
            StringBuilder hex = new StringBuilder(64);
145
            for (byte b : hash) {
146
                hex.append(String.format("%02x", b));
33756 ranu 147
            }
36510 amit 148
            return hex.toString();
149
        } catch (Exception e) {
150
            throw new RuntimeException("SHA-256 not available", e);
33756 ranu 151
        }
152
    }
153
}