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