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.internal;
17
18 import com.amazonaws.ClientConfiguration;
19 import com.amazonaws.Request;
20 import com.amazonaws.annotation.SdkInternalApi;
21 import com.amazonaws.annotation.ThreadSafe;
22 import com.amazonaws.auth.SdkClock;
23 import com.amazonaws.auth.internal.AWS4SignerUtils;
24 import com.amazonaws.handlers.HandlerContextKey;
25 import com.amazonaws.retry.ClockSkewAdjuster;
26 import com.amazonaws.retry.RetryPolicyAdapter;
27 import com.amazonaws.retry.v2.RetryPolicy;
28 import java.util.ArrayList;
29 import java.util.List;
30
31 /**
32  * Provides SDK request header "amz-sdk-request"
33  */

34 @ThreadSafe
35 @SdkInternalApi
36 public final class SdkRequestRetryHeaderProvider {
37     private static final String SDK_REQUEST_RETRY_HEADER = "amz-sdk-request";
38     private final ClientConfiguration config;
39     private final Integer maxErrorRetry;
40     private final ClockSkewAdjuster clockSkewAdjuster;
41
42     public SdkRequestRetryHeaderProvider(ClientConfiguration config,
43                                          RetryPolicy retryPolicy,
44                                          ClockSkewAdjuster clockSkewAdjuster) {
45         this.config = config;
46         if (retryPolicy instanceof RetryPolicyAdapter) {
47             maxErrorRetry = ((RetryPolicyAdapter) retryPolicy).getMaxErrorRetry() + 1;
48         } else {
49             maxErrorRetry = null;
50         }
51         this.clockSkewAdjuster = clockSkewAdjuster;
52     }
53
54     public void addSdkRequestRetryHeader(Request<?> request, int attemptNum) {
55         List<Pair> pairs = requestPairs(request, String.valueOf(attemptNum));
56
57         StringBuilder headerValue = new StringBuilder();
58         for (Pair pair: pairs) {
59             headerValue.append(pair.name).append("=").append(pair.value).append(";");
60         }
61
62         String header = headerValue.toString().substring(0, headerValue.length() - 1);
63         request.addHeader(SDK_REQUEST_RETRY_HEADER, header);
64     }
65
66     private List<Pair> requestPairs(Request<?> request, String attemptNum) {
67         List<Pair> requestPairs = new ArrayList<Pair>();
68
69         String optionalTtl = calculateTtl(request);
70         if (optionalTtl != null) {
71             requestPairs.add(new Pair("ttl", optionalTtl));
72         }
73
74         requestPairs.add(new Pair("attempt", attemptNum));
75
76         if (maxErrorRetry != null) {
77             requestPairs.add(new Pair("max", String.valueOf(maxErrorRetry)));
78         }
79
80         return requestPairs;
81     }
82
83     /**
84      * Calculate the ttl and format it:
85      *
86      * ttl = current_time + socket_read_timeout + estimated_skew
87      * estimated_skew_i = timeStampFromDateHeader_i-1 - timeClientReceivedResposne_i-1
88      */

89     private String calculateTtl(Request<?> request) {
90         // ttl should be omitted for streaming operations
91         if (isStreaming(request)) {
92             return null;
93         }
94
95         Integer estimatedSkew = clockSkewAdjuster.getEstimatedSkew();
96         // Calculating this value requires that we've seen at least one response from the service s
97         // o the ttl SHOULD be omitted for the initial request.
98         if (estimatedSkew == null) {
99             return null;
100         }
101
102         long currentTimeMillis = SdkClock.Instance.get().currentTimeMillis();
103
104         // The returned estimatedSkew is derived from (responseTime - clientTime), so -estimatedSkew = clientTime - responseTime
105         long ttl = currentTimeMillis + config.getSocketTimeout() - estimatedSkew * 1000;
106
107         return AWS4SignerUtils.formatTimestamp(ttl);
108     }
109
110     private boolean isStreaming(Request<?> request) {
111         return Boolean.TRUE.equals(request.getHandlerContext(HandlerContextKey.HAS_STREAMING_INPUT)) ||
112                Boolean.TRUE.equals(request.getHandlerContext(HandlerContextKey.HAS_STREAMING_OUTPUT));
113     }
114
115     private static final class Pair {
116         private String name;
117         private String value;
118
119         Pair(String name, String value) {
120             this.name = name;
121             this.value = value;
122         }
123     }
124 }
125