• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.libraries.entitlement.http;
18 
19 import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_HTTP_STATUS_NOT_SUCCESS;
20 import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE;
21 import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_SERVER_NOT_CONNECTABLE;
22 import static com.android.libraries.entitlement.http.HttpConstants.RequestMethod.POST;
23 import static com.android.libraries.entitlement.utils.DebugUtils.logPii;
24 
25 import static com.google.common.base.Strings.nullToEmpty;
26 
27 import static java.nio.charset.StandardCharsets.UTF_8;
28 import static java.util.concurrent.TimeUnit.SECONDS;
29 
30 import android.net.Network;
31 import android.text.TextUtils;
32 import android.util.Log;
33 
34 import androidx.annotation.WorkerThread;
35 
36 import com.android.libraries.entitlement.ServiceEntitlementException;
37 import com.android.libraries.entitlement.http.HttpConstants.ContentType;
38 import com.android.libraries.entitlement.utils.StreamUtils;
39 
40 import com.google.common.collect.ImmutableList;
41 import com.google.common.net.HttpHeaders;
42 
43 import java.io.DataOutputStream;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.io.OutputStream;
47 import java.net.HttpURLConnection;
48 import java.net.URL;
49 import java.net.URLConnection;
50 import java.util.List;
51 import java.util.Map;
52 
53 /** Implement the HTTP request method according to TS.43 specification. */
54 public class HttpClient {
55     private static final String TAG = "ServiceEntitlement";
56 
57     private HttpURLConnection mConnection;
58 
59     @WorkerThread
request(HttpRequest request)60     public HttpResponse request(HttpRequest request) throws ServiceEntitlementException {
61         logPii("HttpClient.request url: " + request.url());
62         createConnection(request);
63         logPii("HttpClient.request headers (partial): " + mConnection.getRequestProperties());
64         try {
65             if (POST.equals(request.requestMethod())) {
66                 try (OutputStream out = new DataOutputStream(mConnection.getOutputStream())) {
67                     // Android JSON toString() escapes forward-slash with back-slash. It's not
68                     // supported by some vendor and not mandatory in JSON spec. Undo escaping.
69                     String postData = request.postData().toString().replace("\\/", "/");
70                     out.write(postData.getBytes(UTF_8));
71                     logPii("HttpClient.request post data: " + postData);
72                 }
73             }
74             mConnection.connect(); // This is to trigger SocketTimeoutException early
75             HttpResponse response = getHttpResponse(mConnection);
76             Log.d(TAG, "HttpClient.response : " + response);
77             return response;
78         } catch (IOException ioe) {
79             throw new ServiceEntitlementException(
80                     ERROR_HTTP_STATUS_NOT_SUCCESS,
81                     StreamUtils.inputStreamToStringSafe(mConnection.getErrorStream()),
82                     ioe);
83         } finally {
84             closeConnection();
85         }
86     }
87 
createConnection(HttpRequest request)88     private void createConnection(HttpRequest request) throws ServiceEntitlementException {
89         try {
90             URL url = new URL(request.url());
91             Network network = request.network();
92             if (network == null) {
93                 mConnection = (HttpURLConnection) url.openConnection();
94             } else {
95                 mConnection = (HttpURLConnection) network.openConnection(url);
96             }
97 
98             // add HTTP headers
99             for (Map.Entry<String, String> entry : request.requestProperties().entries()) {
100                 mConnection.addRequestProperty(entry.getKey(), entry.getValue());
101             }
102 
103             // set parameters
104             mConnection.setRequestMethod(request.requestMethod());
105             mConnection.setConnectTimeout((int) SECONDS.toMillis(request.timeoutInSec()));
106             mConnection.setReadTimeout((int) SECONDS.toMillis(request.timeoutInSec()));
107             if (POST.equals(request.requestMethod())) {
108                 mConnection.setDoOutput(true);
109             }
110         } catch (IOException ioe) {
111             throw new ServiceEntitlementException(
112                     ERROR_SERVER_NOT_CONNECTABLE, "Configure connection failed!", ioe);
113         }
114     }
115 
closeConnection()116     private void closeConnection() {
117         if (mConnection != null) {
118             mConnection.disconnect();
119             mConnection = null;
120         }
121     }
122 
getHttpResponse(HttpURLConnection connection)123     private static HttpResponse getHttpResponse(HttpURLConnection connection)
124             throws ServiceEntitlementException {
125         HttpResponse.Builder responseBuilder = HttpResponse.builder();
126         responseBuilder.setContentType(getContentType(connection));
127         try {
128             int responseCode = connection.getResponseCode();
129             logPii("HttpClient.response headers: " + connection.getHeaderFields());
130             if (responseCode != HttpURLConnection.HTTP_OK) {
131                 throw new ServiceEntitlementException(ERROR_HTTP_STATUS_NOT_SUCCESS, responseCode,
132                         connection.getHeaderField(HttpHeaders.RETRY_AFTER),
133                         "Invalid connection response");
134             }
135             responseBuilder.setResponseCode(responseCode);
136             responseBuilder.setResponseMessage(nullToEmpty(connection.getResponseMessage()));
137         } catch (IOException e) {
138             throw new ServiceEntitlementException(
139                     ERROR_HTTP_STATUS_NOT_SUCCESS, "Read response code failed!", e);
140         }
141         responseBuilder.setCookies(getCookies(connection));
142         try {
143             String responseBody = readResponse(connection);
144             logPii("HttpClient.response body: " + responseBody);
145             responseBuilder.setBody(responseBody);
146         } catch (IOException e) {
147             throw new ServiceEntitlementException(
148                     ERROR_MALFORMED_HTTP_RESPONSE, "Read response body/message failed!", e);
149         }
150         return responseBuilder.build();
151     }
152 
readResponse(URLConnection connection)153     private static String readResponse(URLConnection connection) throws IOException {
154         try (InputStream in = connection.getInputStream()) {
155             return StreamUtils.inputStreamToStringSafe(in);
156         }
157     }
158 
getContentType(URLConnection connection)159     private static int getContentType(URLConnection connection) {
160         String contentType = connection.getHeaderField(ContentType.NAME);
161         if (TextUtils.isEmpty(contentType)) {
162             return ContentType.UNKNOWN;
163         }
164 
165         if (contentType.contains("xml")) {
166             return ContentType.XML;
167         } else if ("text/vnd.wap.connectivity".equals(contentType)) {
168             // Workaround that a server vendor uses this type for XML
169             return ContentType.XML;
170         } else if (contentType.contains("json")) {
171             return ContentType.JSON;
172         }
173         return ContentType.UNKNOWN;
174     }
175 
getCookies(URLConnection connection)176     private static List<String> getCookies(URLConnection connection) {
177         List<String> cookies = connection.getHeaderFields().get(HttpHeaders.SET_COOKIE);
178         return cookies == null ? ImmutableList.of() : cookies;
179     }
180 }
181