1 /*
2  * Copyright 2018 The Android Open Source Project
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://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.pump.util;
18 
19 import android.Manifest;
20 import android.net.TrafficStats;
21 
22 import androidx.annotation.NonNull;
23 import androidx.annotation.Nullable;
24 import androidx.annotation.RequiresPermission;
25 import androidx.annotation.WorkerThread;
26 
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.io.OutputStream;
30 import java.net.HttpURLConnection;
31 import java.net.URL;
32 import java.util.ArrayList;
33 import java.util.Collections;
34 import java.util.Comparator;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.TreeMap;
38 import java.util.concurrent.TimeUnit;
39 
40 @WorkerThread
41 public final class Http {
42     private static final String TAG = Clog.tag(Http.class);
43 
44     private static final int TRAFFIC_STATS_TAG = 4711; // TODO Assign a better value
45     private static final byte[] EMPTY_DATA = new byte[0];
46 
Http()47     private Http() { }
48 
49     @RequiresPermission(Manifest.permission.INTERNET)
post(@onNull String uri)50     public static @NonNull byte[] post(@NonNull String uri) throws IOException {
51         return post(uri, Headers.NONE, EMPTY_DATA);
52     }
53 
54     @RequiresPermission(Manifest.permission.INTERNET)
post(@onNull String uri, @NonNull Headers headers)55     public static @NonNull byte[] post(@NonNull String uri, @NonNull Headers headers)
56             throws IOException {
57         return post(uri, headers, EMPTY_DATA);
58     }
59 
60     @RequiresPermission(Manifest.permission.INTERNET)
post(@onNull String uri, @NonNull byte[] data)61     public static @NonNull byte[] post(@NonNull String uri, @NonNull byte[] data)
62             throws IOException {
63         return post(uri, Headers.NONE, data);
64     }
65 
66     @RequiresPermission(Manifest.permission.INTERNET)
post(@onNull String uri, @NonNull Headers headers, @NonNull byte[] data)67     public @NonNull static byte[] post(@NonNull String uri, @NonNull Headers headers,
68             @NonNull byte[] data) throws IOException {
69         return getOrPost(uri, headers, data);
70     }
71 
72     @RequiresPermission(Manifest.permission.INTERNET)
get(@onNull String uri)73     public static @NonNull byte[] get(@NonNull String uri) throws IOException {
74         return get(uri, Headers.NONE);
75     }
76 
77     @RequiresPermission(Manifest.permission.INTERNET)
get(@onNull String uri, @NonNull Headers headers)78     public static @NonNull byte[] get(@NonNull String uri, @NonNull Headers headers)
79             throws IOException {
80         return getOrPost(uri, headers, null);
81     }
82 
getOrPost(String uri, Headers headers, byte[] data)83     private static byte[] getOrPost(String uri, Headers headers, byte[] data) throws IOException {
84         final URL url = new URL(uri);
85         int numRetries = 3;
86         for (;;) {
87             long retryDelaySec = 5;
88             try {
89                 return getOrPost(url, headers, data);
90             } catch (Http.HttpError e) {
91                 int responseCode = e.getResponseCode();
92                 if (responseCode == HttpURLConnection.HTTP_UNAVAILABLE) {
93                     String retryAfter = e.getHeaders().getField("Retry-After");
94                     if (retryAfter != null) {
95                         retryDelaySec = Math.max(0, Long.valueOf(retryAfter));
96                     }
97                 } else if (responseCode != HttpURLConnection.HTTP_GATEWAY_TIMEOUT) {
98                     throw e;
99                 }
100                 if (numRetries-- <= 0) {
101                     throw e;
102                 }
103             } catch (IOException e) {
104                 if (numRetries-- <= 0) {
105                     throw e;
106                 }
107             }
108 
109             if (retryDelaySec > 0) {
110                 try {
111                     Thread.sleep(TimeUnit.SECONDS.toMillis(retryDelaySec));
112                 } catch (InterruptedException e) {
113                     Clog.w(TAG, "Interrupted waiting for retry", e);
114                     throw new IOException(e);
115                 }
116             }
117         }
118     }
119 
getOrPost(URL url, Headers headers, byte[] data)120     private static byte[] getOrPost(URL url, Headers headers, byte[] data) throws IOException {
121         HttpURLConnection connection = null;
122         OutputStream outputStream = null;
123         InputStream inputStream = null;
124         final int oldTag = TrafficStats.getThreadStatsTag();
125         try {
126             TrafficStats.setThreadStatsTag(TRAFFIC_STATS_TAG);
127             connection = (HttpURLConnection) url.openConnection();
128             headers.apply(connection);
129 
130             if (data != null) {
131                 connection.setDoOutput(true);
132                 connection.setFixedLengthStreamingMode(data.length);
133 
134                 outputStream = connection.getOutputStream();
135                 IoUtils.writeToStream(outputStream, data);
136                 checkResponseCode(connection);
137             }
138 
139             checkResponseCode(connection);
140             inputStream = connection.getInputStream();
141             return IoUtils.readFromStream(inputStream);
142         } finally {
143             IoUtils.close(inputStream);
144             IoUtils.close(outputStream);
145             disconnect(connection);
146             TrafficStats.setThreadStatsTag(oldTag);
147         }
148     }
149 
checkResponseCode(HttpURLConnection connection)150     private static void checkResponseCode(HttpURLConnection connection) throws IOException {
151         int responseCode = connection.getResponseCode();
152         if (responseCode == HttpURLConnection.HTTP_OK) return;
153         String responseMessage = connection.getResponseMessage();
154         Headers responseHeaders = new Headers(connection.getHeaderFields());
155 
156         InputStream errorStream = null;
157         try {
158             errorStream = connection.getErrorStream();
159             if (errorStream != null) {
160                 byte[] responseBody = IoUtils.readFromStream(errorStream);
161                 throw new HttpError(responseCode, responseMessage, responseHeaders, responseBody);
162             }
163             throw new HttpError(responseCode, responseMessage, responseHeaders);
164         } finally {
165             IoUtils.close(errorStream);
166         }
167     }
168 
disconnect(HttpURLConnection connection)169     private static void disconnect(HttpURLConnection connection) {
170         if (connection == null) return;
171         connection.disconnect();
172     }
173 
174     public static final class ContentType {
ContentType()175         private ContentType() { }
176     }
177 
178     public static final class Headers {
179         private final Map<String, List<String>> mFields;
180 
181         public static final Headers NONE = new Headers.Builder().build();
182 
create(String contentType)183         private static Headers create(String contentType) {
184             return new Headers.Builder().set("Content-Type", contentType).build();
185         }
186 
Headers(Map<String, List<String>> fields)187         private Headers(Map<String, List<String>> fields) {
188             mFields = fields;
189         }
190 
apply(@onNull HttpURLConnection connection)191         public void apply(@NonNull HttpURLConnection connection) {
192             for (Map.Entry<String, List<String>> entry : mFields.entrySet()) {
193                 boolean first = true;
194                 String key = entry.getKey();
195                 for (String value: entry.getValue()) {
196                     if (first) {
197                         first = false;
198                         connection.setRequestProperty(key, value);
199                     } else {
200                         connection.addRequestProperty(key, value);
201                     }
202                 }
203             }
204         }
205 
getField(@onNull String key)206         public @Nullable String getField(@NonNull String key) {
207             List<String> values = getFieldValues(key);
208             return values == null ? null : values.get(0);
209         }
210 
getFieldValues(@onNull String key)211         public @Nullable List<String> getFieldValues(@NonNull String key) {
212             return getFields().get(key);
213         }
214 
getFields()215         public @NonNull Map<String, List<String>> getFields() {
216             return mFields;
217         }
218 
219         public static final class Builder {
220             private static final Comparator<String> FIELD_NAME_COMPARATOR = (a, b) -> {
221                 //noinspection StringEquality
222                 if (a == b) {
223                     return 0;
224                 } else if (a == null) {
225                     return -1;
226                 } else if (b == null) {
227                     return 1;
228                 } else {
229                     return String.CASE_INSENSITIVE_ORDER.compare(a, b);
230                 }
231             };
232             private final List<String> mNamesAndValues = new ArrayList<>();
233 
Builder()234             public Builder() { }
235 
Builder(@onNull Headers headers)236             public Builder(@NonNull Headers headers) {
237                 for (Map.Entry<String, List<String>> entry : headers.mFields.entrySet()) {
238                     for (String value: entry.getValue()) {
239                         mNamesAndValues.add(entry.getKey());
240                         mNamesAndValues.add(value);
241                     }
242                 }
243             }
244 
add(@onNull String fieldName, @NonNull String value)245             public @NonNull Builder add(@NonNull String fieldName, @NonNull String value) {
246                 mNamesAndValues.add(fieldName);
247                 mNamesAndValues.add(value);
248                 return this;
249             }
250 
set(@onNull String fieldName, @NonNull String value)251             public @NonNull Builder set(@NonNull String fieldName, @NonNull String value) {
252                 return removeAll(fieldName).add(fieldName, value);
253             }
254 
removeAll(String fieldName)255             private Builder removeAll(String fieldName) {
256                 for (int i = 0; i < mNamesAndValues.size(); i += 2) {
257                     if (fieldName.equalsIgnoreCase(mNamesAndValues.get(i))) {
258                         mNamesAndValues.remove(i);
259                         mNamesAndValues.remove(i);
260                     }
261                 }
262                 return this;
263             }
264 
build()265             public @NonNull Headers build() {
266                 Map<String, List<String>> headers = new TreeMap<>(FIELD_NAME_COMPARATOR);
267 
268                 for (int i = 0; i < mNamesAndValues.size(); i += 2) {
269                     String fieldName = mNamesAndValues.get(i);
270                     String value = mNamesAndValues.get(i + 1);
271 
272                     List<String> values = new ArrayList<>();
273                     List<String> others = headers.get(fieldName);
274                     if (others != null) {
275                         values.addAll(others);
276                     }
277                     values.add(value);
278                     headers.put(fieldName, Collections.unmodifiableList(values));
279                 }
280 
281                 return new Headers(Collections.unmodifiableMap(headers));
282             }
283         }
284     }
285 
286     public static final class HttpError extends IOException {
287         private static final long serialVersionUID = 1L;
288 
289         private final int mCode;
290         private final String mMessage;
291         private final Headers mHeaders;
292         private final byte[] mBody;
293 
HttpError(int code, String message, Headers headers)294         private HttpError(int code, String message, Headers headers) {
295             this(code, message, headers, null);
296         }
297 
HttpError(int code, String message, Headers headers, byte[] body)298         private HttpError(int code, String message, Headers headers, byte[] body) {
299             super(code + " " + message);
300             mCode = code;
301             mMessage = message;
302             mHeaders = headers;
303             mBody = body;
304         }
305 
getResponseCode()306         public int getResponseCode() {
307             return mCode;
308         }
309 
getResponseMessage()310         public @NonNull String getResponseMessage() {
311             return mMessage;
312         }
313 
getHeaders()314         public @NonNull Headers getHeaders() {
315             return mHeaders;
316         }
317 
getResponseBody()318         public @Nullable byte[] getResponseBody() {
319             return mBody;
320         }
321     }
322 }
323