1
15
16 package com.amazonaws.retry;
17
18 import com.amazonaws.AmazonServiceException;
19 import com.amazonaws.Request;
20 import com.amazonaws.SdkBaseException;
21 import com.amazonaws.annotation.NotThreadSafe;
22 import com.amazonaws.annotation.SdkInternalApi;
23 import com.amazonaws.annotation.SdkTestInternalApi;
24 import com.amazonaws.annotation.ThreadSafe;
25 import com.amazonaws.auth.internal.AWS4SignerUtils;
26 import com.amazonaws.util.DateUtils;
27 import com.amazonaws.util.ValidationUtils;
28 import java.util.Collections;
29 import java.util.Date;
30 import java.util.HashSet;
31 import java.util.Set;
32 import org.apache.commons.logging.Log;
33 import org.apache.commons.logging.LogFactory;
34 import org.apache.http.Header;
35 import org.apache.http.HttpResponse;
36
37
43 @ThreadSafe
44 @SdkInternalApi
45 public final class ClockSkewAdjuster {
46 private static final Log log = LogFactory.getLog(ClockSkewAdjuster.class);
47
48
51 private static final Set<Integer> AUTHENTICATION_ERROR_STATUS_CODES;
52
53
57 private static final int CLOCK_SKEW_ADJUST_THRESHOLD_IN_SECONDS = 4 * 60;
58
59 private volatile Integer estimatedSkew;
60
61 static {
62 Set<Integer> statusCodes = new HashSet<Integer>();
63 statusCodes.add(401);
64 statusCodes.add(403);
65 AUTHENTICATION_ERROR_STATUS_CODES = Collections.unmodifiableSet(statusCodes);
66 }
67
68
73 public Integer getEstimatedSkew() {
74 return estimatedSkew;
75 }
76
77 public void updateEstimatedSkew(AdjustmentRequest adjustmentRequest) {
78 try {
79 Date serverDate = getServerDate(adjustmentRequest);
80
81 if (serverDate != null) {
82 estimatedSkew = timeSkewInSeconds(getCurrentDate(adjustmentRequest), serverDate);
83 }
84 } catch(RuntimeException exception) {
85 log.debug("Unable to update estimated skew.", exception);
86 }
87 }
88
89
92 public ClockSkewAdjustment getAdjustment(AdjustmentRequest adjustmentRequest) {
93 ValidationUtils.assertNotNull(adjustmentRequest, "adjustmentRequest");
94 ValidationUtils.assertNotNull(adjustmentRequest.exception, "adjustmentRequest.exception");
95 ValidationUtils.assertNotNull(adjustmentRequest.clientRequest, "adjustmentRequest.clientRequest");
96 ValidationUtils.assertNotNull(adjustmentRequest.serviceResponse, "adjustmentRequest.serviceResponse");
97
98 int timeSkewInSeconds = 0;
99 boolean isAdjustmentRecommended = false;
100
101 try {
102 if (isAdjustmentRecommended(adjustmentRequest)) {
103 Date serverDate = getServerDate(adjustmentRequest);
104
105 if (serverDate != null) {
106 timeSkewInSeconds = timeSkewInSeconds(getCurrentDate(adjustmentRequest), serverDate);
107 isAdjustmentRecommended = true;
108 }
109 }
110 } catch (RuntimeException e) {
111 log.warn("Unable to correct for clock skew.", e);
112 }
113
114 return new ClockSkewAdjustment(isAdjustmentRecommended, timeSkewInSeconds);
115 }
116
117 private boolean isAdjustmentRecommended(AdjustmentRequest adjustmentRequest) {
118 if (!(adjustmentRequest.exception instanceof AmazonServiceException)) {
119 return false;
120 }
121
122 AmazonServiceException exception = (AmazonServiceException) adjustmentRequest.exception;
123
124 return isDefinitelyClockSkewError(exception) ||
125 (mayBeClockSkewError(exception) && clientRequestWasSkewed(adjustmentRequest));
126 }
127
128 private boolean isDefinitelyClockSkewError(AmazonServiceException exception) {
129 return RetryUtils.isClockSkewError(exception);
130 }
131
132 private boolean mayBeClockSkewError(AmazonServiceException exception) {
133 return AUTHENTICATION_ERROR_STATUS_CODES.contains(exception.getStatusCode());
134 }
135
136 private boolean clientRequestWasSkewed(AdjustmentRequest adjustmentRequest) {
137 Date serverDate = getServerDate(adjustmentRequest);
138 if (serverDate == null) {
139 return false;
140 }
141
142 int requestClockSkew = timeSkewInSeconds(getClientDate(adjustmentRequest), serverDate);
143 return Math.abs(requestClockSkew) > CLOCK_SKEW_ADJUST_THRESHOLD_IN_SECONDS;
144 }
145
146
151 private int timeSkewInSeconds(Date clientTime, Date serverTime) {
152 ValidationUtils.assertNotNull(clientTime, "clientTime");
153 ValidationUtils.assertNotNull(serverTime, "serverTime");
154
155 long value = (clientTime.getTime() - serverTime.getTime()) / 1000;
156
157 if ((int) value != value) {
158 throw new IllegalStateException("Time is too skewed to adjust: (clientTime: " + clientTime.getTime() + ", " +
159 "serverTime: " + serverTime.getTime() + ")");
160 }
161 return (int) value;
162 }
163
164 private Date getCurrentDate(AdjustmentRequest adjustmentRequest) {
165 return new Date(adjustmentRequest.currentTime);
166 }
167
168 private Date getClientDate(AdjustmentRequest adjustmentRequest) {
169 return new Date(adjustmentRequest.currentTime - (long)(adjustmentRequest.clientRequest.getTimeOffset() * 1000));
170 }
171
172 private Date getServerDate(AdjustmentRequest adjustmentRequest) {
173 String serverDateStr = null;
174 try {
175 Header[] responseDateHeader = adjustmentRequest.serviceResponse.getHeaders("Date");
176
177 if (responseDateHeader.length > 0) {
178 serverDateStr = responseDateHeader[0].getValue();
179 log.debug("Reported server date (from 'Date' header): " + serverDateStr);
180 return DateUtils.parseRFC822Date(serverDateStr);
181 }
182
183 if (adjustmentRequest.exception == null) {
184 return null;
185 }
186
187
188 final String exceptionMessage = adjustmentRequest.exception.getMessage();
189 serverDateStr = getServerDateFromException(exceptionMessage);
190
191 if (serverDateStr != null) {
192 log.debug("Reported server date (from exception message): " + serverDateStr);
193 return DateUtils.parseCompressedISO8601Date(serverDateStr);
194 }
195
196 log.debug("Server did not return a date, so clock skew adjustments will not be applied.");
197 return null;
198 } catch (RuntimeException e) {
199 log.warn("Unable to parse clock skew offset from response: " + serverDateStr, e);
200 return null;
201 }
202 }
203
204
217 private String getServerDateFromException(String body) {
218 final int startPos = body.indexOf("(");
219 int endPos = body.indexOf(" + ");
220 if (endPos == -1) {
221 endPos = body.indexOf(" - ");
222 }
223 return endPos == -1 ? null : body.substring(startPos + 1, endPos);
224 }
225
226 @NotThreadSafe
227 public static final class AdjustmentRequest {
228 private Request<?> clientRequest;
229 private HttpResponse serviceResponse;
230 private SdkBaseException exception;
231 private long currentTime = System.currentTimeMillis();
232
233 public AdjustmentRequest clientRequest(Request<?> clientRequest) {
234 this.clientRequest = clientRequest;
235 return this;
236 }
237
238 public AdjustmentRequest serviceResponse(HttpResponse serviceResponse) {
239 this.serviceResponse = serviceResponse;
240 return this;
241 }
242
243 public AdjustmentRequest exception(SdkBaseException exception) {
244 this.exception = exception;
245 return this;
246 }
247
248 @SdkTestInternalApi
249 public AdjustmentRequest currentTime(long currentTime) {
250 this.currentTime = currentTime;
251 return this;
252 }
253 }
254
255 @ThreadSafe
256 public static final class ClockSkewAdjustment {
257 private final boolean shouldAdjustForSkew;
258 private final int adjustmentInSeconds;
259
260 private ClockSkewAdjustment(boolean shouldAdjust, int adjustmentInSeconds) {
261 this.shouldAdjustForSkew = shouldAdjust;
262 this.adjustmentInSeconds = adjustmentInSeconds;
263 }
264
265 public boolean shouldAdjustForSkew() {
266 return shouldAdjustForSkew;
267 }
268
269 public int inSeconds() {
270 if (!shouldAdjustForSkew) {
271 throw new IllegalStateException("An adjustment is not recommended.");
272 }
273
274 return adjustmentInSeconds;
275 }
276 }
277 }
278