From c9d91ae35c51640ad32c83bf05de90700fcb37a1 Mon Sep 17 00:00:00 2001 From: F43nd1r Date: Fri, 3 Mar 2017 15:18:57 +0100 Subject: [PATCH 1/2] update buildtoolsversion --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c1997d581..ebfd15cdc 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ ext { android { compileSdkVersion androidVersion - buildToolsVersion "23.0.3" + buildToolsVersion '25.0.0' lintOptions { abortOnError false From bbd6cee2d6ed095166691c5cfe8ac528acc55f0d Mon Sep 17 00:00:00 2001 From: F43nd1r Date: Fri, 3 Mar 2017 16:34:01 +0100 Subject: [PATCH 2/2] increase flexibility in HttpSender & HttpRequest --- src/main/java/org/acra/ACRAConstants.java | 2 + .../org/acra/file/CrashReportPersister.java | 3 +- .../org/acra/http/DefaultHttpRequest.java | 201 ++++++++++++++++++ src/main/java/org/acra/http/HttpRequest.java | 29 +++ src/main/java/org/acra/sender/HttpSender.java | 57 +++-- src/main/java/org/acra/util/HttpRequest.java | 194 ----------------- 6 files changed, 271 insertions(+), 215 deletions(-) create mode 100644 src/main/java/org/acra/http/DefaultHttpRequest.java create mode 100644 src/main/java/org/acra/http/HttpRequest.java delete mode 100644 src/main/java/org/acra/util/HttpRequest.java diff --git a/src/main/java/org/acra/ACRAConstants.java b/src/main/java/org/acra/ACRAConstants.java index 7d4e837c7..4500f11d5 100644 --- a/src/main/java/org/acra/ACRAConstants.java +++ b/src/main/java/org/acra/ACRAConstants.java @@ -164,4 +164,6 @@ private ACRAConstants(){} public static final String DEFAULT_CERTIFICATE_TYPE = "X.509"; public static final Element NOT_AVAILABLE = new StringElement("N/A"); + + public static final String UTF8 = "UTF-8"; } diff --git a/src/main/java/org/acra/file/CrashReportPersister.java b/src/main/java/org/acra/file/CrashReportPersister.java index 79a51c88b..aa275ba45 100644 --- a/src/main/java/org/acra/file/CrashReportPersister.java +++ b/src/main/java/org/acra/file/CrashReportPersister.java @@ -44,7 +44,6 @@ * file. */ public final class CrashReportPersister { - private static final String CHARSET = "UTF-8"; /** * Loads properties from the specified {@code File}. @@ -76,7 +75,7 @@ public CrashReportData load(@NonNull File file) throws IOException, JSONExceptio */ public void store(@NonNull CrashReportData crashData, @NonNull File file) throws IOException { - final OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file), CHARSET); + final OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file), ACRAConstants.UTF8); try { writer.write(crashData.toJSON().toString()); writer.flush(); diff --git a/src/main/java/org/acra/http/DefaultHttpRequest.java b/src/main/java/org/acra/http/DefaultHttpRequest.java new file mode 100644 index 000000000..bd81549a9 --- /dev/null +++ b/src/main/java/org/acra/http/DefaultHttpRequest.java @@ -0,0 +1,201 @@ +/* + * 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.sender.HttpSender.Type; +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 class DefaultHttpRequest implements HttpRequest { + + @NonNull + private final ACRAConfiguration config; + @NonNull + private final Context context; + @NonNull + private final Method method; + @NonNull + private final Type type; + private String login; + private String password; + private int connectionTimeOut = 3000; + private int socketTimeOut = 3000; + private Map headers; + + public DefaultHttpRequest(@NonNull ACRAConfiguration config, @NonNull Context context, @NonNull Method method, @NonNull Type type, + @Nullable String login, @Nullable String password, int connectionTimeOut, int socketTimeOut, @Nullable Map headers) { + this.config = config; + this.context = context; + this.method = method; + this.type = type; + this.login = login; + this.password = password; + this.connectionTimeOut = connectionTimeOut; + this.socketTimeOut = socketTimeOut; + this.headers = headers; + } + + + /** + * Posts to a URL. + * + * @param url URL to which to post. + * @param content Map of parameters to post to a URL. + * @throws IOException if the data cannot be posted. + */ + @Override + public void send(@NonNull URL url, @NonNull String 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, type, login, password, headers); + 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); + } + 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, @NonNull Type type, @Nullable String login, @Nullable String password, + @Nullable Map customHeaders) 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", type.getContentType()); + + // 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 header : customHeaders.entrySet()) { + connection.setRequestProperty(header.getKey(), header.getValue()); + } + } + } + + @SuppressWarnings("WeakerAccess") + protected void writeContent(@NonNull HttpURLConnection connection, @NonNull Method method, @NonNull String content) throws IOException{ + final byte[] contentAsBytes = content.getBytes(ACRAConstants.UTF8); + // 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); + } + } + + @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); + } + } +} diff --git a/src/main/java/org/acra/http/HttpRequest.java b/src/main/java/org/acra/http/HttpRequest.java new file mode 100644 index 000000000..6f929f05c --- /dev/null +++ b/src/main/java/org/acra/http/HttpRequest.java @@ -0,0 +1,29 @@ +/* + * 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.support.annotation.NonNull; + +import java.io.IOException; +import java.net.URL; + +/** + * @author F43nd1r + * @since 03.03.2017 + */ +public interface HttpRequest { + void send(@NonNull URL url, @NonNull String content) throws IOException; +} diff --git a/src/main/java/org/acra/sender/HttpSender.java b/src/main/java/org/acra/sender/HttpSender.java index c017fee58..4369b3730 100644 --- a/src/main/java/org/acra/sender/HttpSender.java +++ b/src/main/java/org/acra/sender/HttpSender.java @@ -28,13 +28,16 @@ import org.acra.collections.ImmutableSet; import org.acra.collector.CrashReportData; import org.acra.config.ACRAConfiguration; +import org.acra.http.DefaultHttpRequest; +import org.acra.http.HttpRequest; import org.acra.model.Element; -import org.acra.util.HttpRequest; import org.json.JSONObject; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; +import java.net.URLEncoder; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -61,6 +64,31 @@ */ public class HttpSender implements ReportSender { + /** + * Converts a Map of parameters into a URL encoded Sting. + * + * @param parameters Map of parameters to convert. + * @return URL encoded String representing the parameters. + * @throws UnsupportedEncodingException if one of the parameters couldn't be converted to UTF-8. + */ + @NonNull + private static String getParamsAsFormString(@NonNull Map parameters) throws UnsupportedEncodingException { + + final StringBuilder dataBfr = new StringBuilder(); + for (final Map.Entry entry : parameters.entrySet()) { + if (dataBfr.length() != 0) { + dataBfr.append('&'); + } + final Object preliminaryValue = entry.getValue(); + final Object value = (preliminaryValue == null) ? "" : preliminaryValue; + dataBfr.append(URLEncoder.encode(entry.getKey().toString(), ACRAConstants.UTF8)); + dataBfr.append('='); + dataBfr.append(URLEncoder.encode(value.toString(), ACRAConstants.UTF8)); + } + + return dataBfr.toString(); + } + /** * Available HTTP methods to send data. Only POST and PUT are currently * supported. @@ -95,7 +123,7 @@ public enum Type { FORM("application/x-www-form-urlencoded") { @Override String convertReport(HttpSender sender, CrashReportData report) throws IOException { - return HttpRequest.getParamsAsFormString(sender.convertToForm(report)); + return getParamsAsFormString(sender.convertToForm(report)); } }, /** @@ -227,15 +255,16 @@ public void send(@NonNull Context context, @NonNull CrashReportData report) thro String baseUrl = mFormUri == null ? config.formUri() : mFormUri.toString(); if (ACRA.DEV_LOGGING) ACRA.log.d(LOG_TAG, "Connect to " + baseUrl); - final HttpRequest request = new HttpRequest(config); - configureHttpRequest(request); + final String login = mUsername != null ? mUsername : isNull(config.formUriBasicAuthLogin()) ? null : config.formUriBasicAuthLogin(); + final String password = mPassword != null ? mPassword : isNull(config.formUriBasicAuthPassword()) ? null : config.formUriBasicAuthPassword(); + final HttpRequest request = createHttpRequest(config, context, mMethod, mType, login, password, config.connectionTimeout(), config.socketTimeout(), config.getHttpHeaders()); // Generate report body depending on requested type final String reportAsString = mType.convertReport(this, report); // Adjust URL depending on method URL reportUrl = mMethod.createURL(baseUrl, report); - request.send(context, reportUrl, mMethod, reportAsString, mType); + request.send(reportUrl, reportAsString); } catch (@NonNull IOException e) { throw new ReportSenderException("Error while sending " + config.reportType() @@ -243,20 +272,10 @@ public void send(@NonNull Context context, @NonNull CrashReportData report) thro } } - /** - * Configure the HttpRequest. Subclasses can perform additional configuration here - * - * @param request the request to configure - */ - @SuppressWarnings({"WeakerAccess"}) - protected void configureHttpRequest(HttpRequest request) { - final String login = mUsername != null ? mUsername : isNull(config.formUriBasicAuthLogin()) ? null : config.formUriBasicAuthLogin(); - final String password = mPassword != null ? mPassword : isNull(config.formUriBasicAuthPassword()) ? null : config.formUriBasicAuthPassword(); - request.setConnectionTimeOut(config.connectionTimeout()); - request.setSocketTimeOut(config.socketTimeout()); - request.setLogin(login); - request.setPassword(password); - request.setHeaders(config.getHttpHeaders()); + @SuppressWarnings("WeakerAccess") + protected HttpRequest createHttpRequest(@NonNull ACRAConfiguration configuration, @NonNull Context context, @NonNull Method method, @NonNull Type type, + @Nullable String login, @Nullable String password, int connectionTimeOut, int socketTimeOut, @Nullable Map headers){ + return new DefaultHttpRequest(configuration, context, method, type, login, password, connectionTimeOut, socketTimeOut, headers); } /** diff --git a/src/main/java/org/acra/util/HttpRequest.java b/src/main/java/org/acra/util/HttpRequest.java deleted file mode 100644 index c5b0eb5be..000000000 --- a/src/main/java/org/acra/util/HttpRequest.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * This class was copied from this Stackoverflow Q&A: - * http://stackoverflow.com/questions/2253061/secure-http-post-in-android/2253280#2253280 - * Thanks go to MattC! - */ -package org.acra.util; - -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.config.ACRAConfiguration; -import org.acra.security.KeyStoreHelper; -import org.acra.sender.HttpSender.Method; -import org.acra.sender.HttpSender.Type; - -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLEncoder; -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; - -public final class HttpRequest { - - private static final String UTF8 = "UTF-8"; - - private final ACRAConfiguration config; - private String login; - private String password; - private int connectionTimeOut = 3000; - private int socketTimeOut = 3000; - private Map headers; - - public HttpRequest(@NonNull ACRAConfiguration config) { - this.config = config; - } - - public void setLogin(@Nullable String login) { - this.login = login; - } - - public void setPassword(@Nullable String password) { - this.password = password; - } - - public void setConnectionTimeOut(int connectionTimeOut) { - this.connectionTimeOut = connectionTimeOut; - } - - public void setSocketTimeOut(int socketTimeOut) { - this.socketTimeOut = socketTimeOut; - } - - public void setHeaders(@Nullable Map headers) { - this.headers = headers; - } - - - /** - * Posts to a URL. - * - * @param url URL to which to post. - * @param content Map of parameters to post to a URL. - * @throws IOException if the data cannot be posted. - */ - public void send(@NonNull Context context, @NonNull URL url, @NonNull Method method, @NonNull String content, @NonNull Type type) throws IOException { - - final HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); - - // Configure SSL - if (urlConnection instanceof HttpsURLConnection) { - try { - final HttpsURLConnection httpsUrlConnection = (HttpsURLConnection) urlConnection; - - 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); - - httpsUrlConnection.setSSLSocketFactory(sslContext.getSocketFactory()); - } catch (GeneralSecurityException e) { - ACRA.log.e(LOG_TAG, "Could not configure SSL for ACRA request to " + url, e); - } - } - - // Set Credentials - if (login != null && password != null) { - final String credentials = login + ':' + password; - final String encoded = new String(Base64.encode(credentials.getBytes(UTF8), Base64.NO_WRAP), UTF8); - urlConnection.setRequestProperty("Authorization", "Basic " + encoded); - } - - urlConnection.setConnectTimeout(connectionTimeOut); - urlConnection.setReadTimeout(socketTimeOut); - - // Set Headers - urlConnection.setRequestProperty("User-Agent", String.format("Android ACRA %1$s", BuildConfig.VERSION_NAME)); //sent ACRA version to server - urlConnection.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"); - urlConnection.setRequestProperty("Content-Type", type.getContentType()); - - if (headers != null) { - for (final Map.Entry header : headers.entrySet()) { - urlConnection.setRequestProperty(header.getKey(), header.getValue()); - } - } - - final byte[] contentAsBytes = content.getBytes(UTF8); - - // write output - see http://developer.android.com/reference/java/net/HttpURLConnection.html - urlConnection.setRequestMethod(method.name()); - urlConnection.setDoOutput(true); - urlConnection.setFixedLengthStreamingMode(contentAsBytes.length); - - // Disable ConnectionPooling because otherwise OkHttp ConnectionPool will try to start a Thread on #connect - System.setProperty("http.keepAlive", "false"); - - urlConnection.connect(); - - final OutputStream outputStream = new BufferedOutputStream(urlConnection.getOutputStream()); - try { - outputStream.write(contentAsBytes); - outputStream.flush(); - } finally { - IOUtils.safeClose(outputStream); - } - - if (ACRA.DEV_LOGGING) ACRA.log.d(LOG_TAG, "Sending request to " + url); - if (ACRA.DEV_LOGGING) ACRA.log.d(LOG_TAG, "Http " + method.name() + " content : "); - if (ACRA.DEV_LOGGING) ACRA.log.d(LOG_TAG, content); - - final int responseCode = urlConnection.getResponseCode(); - if (ACRA.DEV_LOGGING) ACRA.log.d(LOG_TAG, "Request response : " + responseCode + " : " + urlConnection.getResponseMessage()); - 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=" + urlConnection.getResponseMessage()); - 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=" + urlConnection.getResponseMessage()); - } - - urlConnection.disconnect(); - } - - /** - * Converts a Map of parameters into a URL encoded Sting. - * - * @param parameters Map of parameters to convert. - * @return URL encoded String representing the parameters. - * @throws UnsupportedEncodingException if one of the parameters couldn't be converted to UTF-8. - */ - @NonNull - public static String getParamsAsFormString(@NonNull Map parameters) throws UnsupportedEncodingException { - - final StringBuilder dataBfr = new StringBuilder(); - for (final Map.Entry entry : parameters.entrySet()) { - if (dataBfr.length() != 0) { - dataBfr.append('&'); - } - final Object preliminaryValue = entry.getValue(); - final Object value = (preliminaryValue == null) ? "" : preliminaryValue; - dataBfr.append(URLEncoder.encode(entry.getKey().toString(), UTF8)); - dataBfr.append('='); - dataBfr.append(URLEncoder.encode(value.toString(), UTF8)); - } - - return dataBfr.toString(); - } -}