1 /*
2  * Copyright 2019-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at:
7  *
8  *    http://aws.amazon.com/apache2.0
9  *
10  * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
11  * OR CONDITIONS OF ANY KIND, either express or implied. See the
12  * License for the specific language governing permissions and
13  * limitations under the License.
14  */

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 /**
38  * Applies heuristics to suggest a clock skew adjustment that should be applied to future requests based on a given service error.
39  *
40  * This handles cases that are definitely clock skew errors (where {@link RetryUtils#isClockSkewError} is true) as well as
41  * cases that may or may not be clock skew errors.
42  */

43 @ThreadSafe
44 @SdkInternalApi
45 public final class ClockSkewAdjuster {
46     private static final Log log = LogFactory.getLog(ClockSkewAdjuster.class);
47
48     /**
49      * The HTTP status codes associated with authentication errors. These status codes may be caused by skewed clock.
50      */

51     private static final Set<Integer> AUTHENTICATION_ERROR_STATUS_CODES;
52
53     /**
54      * When we get an error that may be due to a clock skew error, and our clock is different than the service clock, this is
55      * the difference threshold (in seconds) beyond which we will recommend a clock skew adjustment.
56      */

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     /**
69      * The estimated skew is the difference between the local time at which the client received the response and the Date
70      * header from the service's response. This time represents both the time difference between the client and server due to
71      * clock differences as well as the network latency for sending a response from the service to the client.
72      */

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     /**
90      * Recommend a {@link ClockSkewAdjustment}, based on the provided {@link AdjustmentRequest}.
91      */

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     /**
147      * Calculate the time skew between a client and server date. This value has the same semantics of
148      * {@link Request#setTimeOffset(int)}. Positive values imply the client clock is "fast" and negative values imply
149      * the client clock is "slow".
150      */

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             // SQS doesn't return Date header
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     /**
205      * Returns date string from the exception message body in form of yyyyMMdd'T'HHmmss'Z' We
206      * needed to extract date from the message body because SQS is the only service that does
207      * not provide date header in the response. Example, when device time is behind than the
208      * server time than we get a string that looks something like this: "Signature expired:
209      * 20130401T030113Z is now earlier than 20130401T034613Z (20130401T040113Z - 15 min.)"
210      *
211      * SWF: Signature not yet current: 20140819T173921Z is still later than 20140819T173829Z
212      * (20140819T173329Z + 5 min.)
213      *
214      * @param body The message from where the server time is being extracted
215      * @return Return datetime in string format (yyyyMMdd'T'HHmmss'Z')
216      */

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