Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Increase flexibility in HttpSender & HttpRequest #557

Merged
merged 2 commits into from
Mar 3, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ ext {

android {
compileSdkVersion androidVersion
buildToolsVersion "23.0.3"
buildToolsVersion '25.0.0'

lintOptions {
abortOnError false
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/acra/ACRAConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
3 changes: 1 addition & 2 deletions src/main/java/org/acra/file/CrashReportPersister.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
* file.
*/
public final class CrashReportPersister {
private static final String CHARSET = "UTF-8";

/**
* Loads properties from the specified {@code File}.
Expand Down Expand Up @@ -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();
Expand Down
201 changes: 201 additions & 0 deletions src/main/java/org/acra/http/DefaultHttpRequest.java
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> 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);
}
}
}
29 changes: 29 additions & 0 deletions src/main/java/org/acra/http/HttpRequest.java
Original file line number Diff line number Diff line change
@@ -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;
}
57 changes: 38 additions & 19 deletions src/main/java/org/acra/sender/HttpSender.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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));
}
},
/**
Expand Down Expand Up @@ -227,36 +255,27 @@ 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()
+ " report via Http " + mMethod.name(), e);
}
}

/**
* 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<String, String> headers){
return new DefaultHttpRequest(configuration, context, method, type, login, password, connectionTimeOut, socketTimeOut, headers);
}

/**
Expand Down
Loading