blob: 9ccb5ee497b8b9ebdbae5366c12fe2aefec7a0f8 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.libraries.entitlement.http;
import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_HTTP_STATUS_NOT_SUCCESS;
import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE;
import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_SERVER_NOT_CONNECTABLE;
import static com.android.libraries.entitlement.http.HttpConstants.RequestMethod.POST;
import static com.android.libraries.entitlement.utils.DebugUtils.logPii;
import static com.google.common.base.Strings.nullToEmpty;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.SECONDS;
import android.net.Network;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.WorkerThread;
import com.android.libraries.entitlement.ServiceEntitlementException;
import com.android.libraries.entitlement.http.HttpConstants.ContentType;
import com.android.libraries.entitlement.utils.StreamUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.net.HttpHeaders;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.List;
import java.util.Map;
/** Implement the HTTP request method according to TS.43 specification. */
public class HttpClient {
private static final String TAG = "ServiceEntitlement";
private HttpURLConnection mConnection;
@WorkerThread
public HttpResponse request(HttpRequest request) throws ServiceEntitlementException {
logPii("HttpClient.request url: " + request.url());
createConnection(request);
logPii("HttpClient.request headers (partial): " + mConnection.getRequestProperties());
try {
if (POST.equals(request.requestMethod())) {
try (OutputStream out = new DataOutputStream(mConnection.getOutputStream())) {
// Android JSON toString() escapes forward-slash with back-slash. It's not
// supported by some vendor and not mandatory in JSON spec. Undo escaping.
String postData = request.postData().toString().replace("\\/", "/");
out.write(postData.getBytes(UTF_8));
logPii("HttpClient.request post data: " + postData);
}
}
mConnection.connect(); // This is to trigger SocketTimeoutException early
HttpResponse response = getHttpResponse(mConnection);
Log.d(TAG, "HttpClient.response : " + response);
return response;
} catch (IOException ioe) {
throw new ServiceEntitlementException(
ERROR_HTTP_STATUS_NOT_SUCCESS,
StreamUtils.inputStreamToStringSafe(mConnection.getErrorStream()),
ioe);
} finally {
closeConnection();
}
}
private void createConnection(HttpRequest request) throws ServiceEntitlementException {
try {
URL url = new URL(request.url());
Network network = request.network();
if (network == null) {
mConnection = (HttpURLConnection) url.openConnection();
} else {
mConnection = (HttpURLConnection) network.openConnection(url);
}
// add HTTP headers
for (Map.Entry<String, String> entry : request.requestProperties().entries()) {
mConnection.addRequestProperty(entry.getKey(), entry.getValue());
}
// set parameters
mConnection.setRequestMethod(request.requestMethod());
mConnection.setConnectTimeout((int) SECONDS.toMillis(request.timeoutInSec()));
mConnection.setReadTimeout((int) SECONDS.toMillis(request.timeoutInSec()));
if (POST.equals(request.requestMethod())) {
mConnection.setDoOutput(true);
}
} catch (IOException ioe) {
throw new ServiceEntitlementException(
ERROR_SERVER_NOT_CONNECTABLE, "Configure connection failed!", ioe);
}
}
private void closeConnection() {
if (mConnection != null) {
mConnection.disconnect();
mConnection = null;
}
}
private static HttpResponse getHttpResponse(HttpURLConnection connection)
throws ServiceEntitlementException {
HttpResponse.Builder responseBuilder = HttpResponse.builder();
responseBuilder.setContentType(getContentType(connection));
try {
int responseCode = connection.getResponseCode();
logPii("HttpClient.response headers: " + connection.getHeaderFields());
if (responseCode != HttpURLConnection.HTTP_OK) {
throw new ServiceEntitlementException(ERROR_HTTP_STATUS_NOT_SUCCESS, responseCode,
connection.getHeaderField(HttpHeaders.RETRY_AFTER),
"Invalid connection response");
}
responseBuilder.setResponseCode(responseCode);
responseBuilder.setResponseMessage(nullToEmpty(connection.getResponseMessage()));
} catch (IOException e) {
throw new ServiceEntitlementException(
ERROR_HTTP_STATUS_NOT_SUCCESS, "Read response code failed!", e);
}
responseBuilder.setCookies(getCookies(connection));
try {
String responseBody = readResponse(connection);
logPii("HttpClient.response body: " + responseBody);
responseBuilder.setBody(responseBody);
} catch (IOException e) {
throw new ServiceEntitlementException(
ERROR_MALFORMED_HTTP_RESPONSE, "Read response body/message failed!", e);
}
return responseBuilder.build();
}
private static String readResponse(URLConnection connection) throws IOException {
try (InputStream in = connection.getInputStream()) {
return StreamUtils.inputStreamToStringSafe(in);
}
}
private static int getContentType(URLConnection connection) {
String contentType = connection.getHeaderField(ContentType.NAME);
if (TextUtils.isEmpty(contentType)) {
return ContentType.UNKNOWN;
}
if (contentType.contains("xml")) {
return ContentType.XML;
} else if ("text/vnd.wap.connectivity".equals(contentType)) {
// Workaround that a server vendor uses this type for XML
return ContentType.XML;
} else if (contentType.contains("json")) {
return ContentType.JSON;
}
return ContentType.UNKNOWN;
}
private static List<String> getCookies(URLConnection connection) {
List<String> cookies = connection.getHeaderFields().get(HttpHeaders.SET_COOKIE);
return cookies == null ? ImmutableList.of() : cookies;
}
}