/* * Copyright (c) 2017 * * 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 org.acra.http; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Base64; import org.acra.ACRA; import org.acra.ACRAConstants; import org.acra.config.ACRAConfiguration; import org.acra.security.KeyStoreHelper; import org.acra.sender.HttpSender.Method; import org.acra.util.IOUtils; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.util.Map; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; import ch.acra.acra.BuildConfig; import static org.acra.ACRA.LOG_TAG; /** * @author F43nd1r * @since 03.03.2017 */ public abstract class BaseHttpRequest<T> implements HttpRequest<T> { @NonNull private final ACRAConfiguration config; @NonNull private final Context context; @NonNull private final Method method; private final String login; private final String password; private final int connectionTimeOut; private final int socketTimeOut; private final Map<String, String> headers; public BaseHttpRequest(@NonNull ACRAConfiguration config, @NonNull Context context, @NonNull Method method, @Nullable String login, @Nullable String password, int connectionTimeOut, int socketTimeOut, @Nullable Map<String, String> headers) { this.config = config; this.context = context; this.method = method; this.login = login; this.password = password; this.connectionTimeOut = connectionTimeOut; this.socketTimeOut = socketTimeOut; this.headers = headers; } /** * Sends to a URL. * * @param url URL to which to send. * @param content content to send. * @throws IOException if the data cannot be sent. */ @Override public void send(@NonNull URL url, @NonNull T content) throws IOException { final HttpURLConnection urlConnection = createConnection(url); if (urlConnection instanceof HttpsURLConnection) { try { configureHttps((HttpsURLConnection) urlConnection); } catch (GeneralSecurityException e) { ACRA.log.e(LOG_TAG, "Could not configure SSL for ACRA request to " + url, e); } } configureTimeouts(urlConnection, connectionTimeOut, socketTimeOut); configureHeaders(urlConnection, login, password, headers, content); if(ACRA.DEV_LOGGING){ ACRA.log.d(LOG_TAG, "Sending request to " + url); ACRA.log.d(LOG_TAG, "Http " + method.name() + " content : "); ACRA.log.d(LOG_TAG, content.toString()); } writeContent(urlConnection, method, content); handleResponse(urlConnection.getResponseCode(), urlConnection.getResponseMessage()); urlConnection.disconnect(); } @SuppressWarnings("WeakerAccess") @NonNull protected HttpURLConnection createConnection(@NonNull URL url) throws IOException { return (HttpURLConnection) url.openConnection(); } @SuppressWarnings("WeakerAccess") protected void configureHttps(@NonNull HttpsURLConnection connection) throws GeneralSecurityException { // Configure SSL final String algorithm = TrustManagerFactory.getDefaultAlgorithm(); final TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm); final KeyStore keyStore = KeyStoreHelper.getKeyStore(context, config); tmf.init(keyStore); final SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, tmf.getTrustManagers(), null); connection.setSSLSocketFactory(sslContext.getSocketFactory()); } @SuppressWarnings("WeakerAccess") protected void configureTimeouts(@NonNull HttpURLConnection connection, int connectionTimeOut, int socketTimeOut){ connection.setConnectTimeout(connectionTimeOut); connection.setReadTimeout(socketTimeOut); } @SuppressWarnings("WeakerAccess") protected void configureHeaders(@NonNull HttpURLConnection connection, @Nullable String login, @Nullable String password, @Nullable Map<String, String> customHeaders, @NonNull T t) throws IOException { // Set Headers connection.setRequestProperty("User-Agent", String.format("Android ACRA %1$s", BuildConfig.VERSION_NAME)); //sent ACRA version to server connection.setRequestProperty("Accept", "text/html,application/xml,application/json,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"); connection.setRequestProperty("Content-Type", getContentType(context, t)); // Set Credentials if (login != null && password != null) { final String credentials = login + ':' + password; final String encoded = new String(Base64.encode(credentials.getBytes(ACRAConstants.UTF8), Base64.NO_WRAP), ACRAConstants.UTF8); connection.setRequestProperty("Authorization", "Basic " + encoded); } if (customHeaders != null) { for (final Map.Entry<String, String> header : customHeaders.entrySet()) { connection.setRequestProperty(header.getKey(), header.getValue()); } } } protected abstract String getContentType(@NonNull Context context, @NonNull T t); @SuppressWarnings("WeakerAccess") protected void writeContent(@NonNull HttpURLConnection connection, @NonNull Method method, @NonNull T content) throws IOException{ final byte[] contentAsBytes = asBytes(content); // write output - see http://developer.android.com/reference/java/net/HttpURLConnection.html connection.setRequestMethod(method.name()); connection.setDoOutput(true); connection.setFixedLengthStreamingMode(contentAsBytes.length); // Disable ConnectionPooling because otherwise OkHttp ConnectionPool will try to start a Thread on #connect System.setProperty("http.keepAlive", "false"); connection.connect(); final OutputStream outputStream = new BufferedOutputStream(connection.getOutputStream()); try { outputStream.write(contentAsBytes); outputStream.flush(); } finally { IOUtils.safeClose(outputStream); } } protected abstract byte[] asBytes(T content) throws IOException; @SuppressWarnings("WeakerAccess") protected void handleResponse(int responseCode, String responseMessage) throws IOException { if (ACRA.DEV_LOGGING) ACRA.log.d(LOG_TAG, "Request response : " + responseCode + " : " + responseMessage); if (responseCode >= HttpURLConnection.HTTP_OK && responseCode < HttpURLConnection.HTTP_MULT_CHOICE) { // All is good ACRA.log.i(LOG_TAG, "Request received by server"); } else if (responseCode == HttpURLConnection.HTTP_CLIENT_TIMEOUT || responseCode >= HttpURLConnection.HTTP_INTERNAL_ERROR) { //timeout or server error. Repeat the request later. ACRA.log.w(LOG_TAG, "Could not send ACRA Post responseCode=" + responseCode + " message=" + responseMessage); throw new IOException("Host returned error code " + responseCode); } else if (responseCode >= HttpURLConnection.HTTP_BAD_REQUEST && responseCode < HttpURLConnection.HTTP_INTERNAL_ERROR) { // Client error. The request must not be repeated. Discard it. ACRA.log.w(LOG_TAG, responseCode + ": Client error - request will be discarded"); } else { ACRA.log.w(LOG_TAG, "Could not send ACRA Post - request will be discarded. responseCode=" + responseCode + " message=" + responseMessage); } } }