Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
* Fixes commons-app#3345
* Trust all hosts for beta
* Added a custom NetworkFetcger for Fresco when on beta

* removed unused assets

* make TestCommonsApplication extend Application instead of Commons Application
  • Loading branch information
ashishkumar468 authored and maskaravivek committed Jan 27, 2020
1 parent df426f7 commit fa87eb5
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 56 deletions.
Binary file removed app/src/main/assets/*.wikimedia.beta.wmflabs.org.cer
Binary file not shown.
20 changes: 17 additions & 3 deletions app/src/main/java/fr/free/nrw/commons/CommonsApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.imagepipeline.core.ImagePipeline;
import com.facebook.imagepipeline.core.ImagePipelineConfig;
import com.facebook.imagepipeline.producers.Consumer;
import com.facebook.imagepipeline.producers.FetchState;
import com.facebook.imagepipeline.producers.NetworkFetcher;
import com.facebook.imagepipeline.producers.ProducerContext;
import com.squareup.leakcanary.LeakCanary;
import com.squareup.leakcanary.RefWatcher;

Expand Down Expand Up @@ -49,6 +53,7 @@
import io.reactivex.internal.functions.Functions;
import io.reactivex.plugins.RxJavaPlugins;
import io.reactivex.schedulers.Schedulers;
import okhttp3.OkHttpClient;
import timber.log.Timber;

import static org.acra.ReportField.ANDROID_VERSION;
Expand Down Expand Up @@ -83,6 +88,9 @@ public class CommonsApplication extends Application {

@Inject @Named("default_preferences") JsonKvStore defaultPrefs;

@Inject
OkHttpClient okHttpClient;

/**
* Constants begin
*/
Expand Down Expand Up @@ -134,9 +142,15 @@ public void onCreate() {
initTimber();

// Set DownsampleEnabled to True to downsample the image in case it's heavy
ImagePipelineConfig config = ImagePipelineConfig.newBuilder(this)
.setDownsampleEnabled(true)
.build();
ImagePipelineConfig.Builder imagePipelineConfigBuilder = ImagePipelineConfig.newBuilder(this)
.setDownsampleEnabled(true);

if(ConfigUtils.isBetaFlavour()){
NetworkFetcher networkFetcher=new CustomNetworkFetcher(okHttpClient);
imagePipelineConfigBuilder.setNetworkFetcher(networkFetcher);
}

ImagePipelineConfig config = imagePipelineConfigBuilder.build();
try {
Fresco.initialize(this, config);
} catch (Exception e) {
Expand Down
206 changes: 206 additions & 0 deletions app/src/main/java/fr/free/nrw/commons/CustomNetworkFetcher.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package fr.free.nrw.commons;

import android.net.Uri;
import android.os.Looper;
import android.os.SystemClock;
import com.facebook.imagepipeline.common.BytesRange;
import com.facebook.imagepipeline.image.EncodedImage;
import com.facebook.imagepipeline.producers.BaseNetworkFetcher;
import com.facebook.imagepipeline.producers.BaseProducerContextCallbacks;
import com.facebook.imagepipeline.producers.Consumer;
import com.facebook.imagepipeline.producers.FetchState;
import com.facebook.imagepipeline.producers.NetworkFetcher;
import com.facebook.imagepipeline.producers.ProducerContext;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
import okhttp3.CacheControl;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

/** Network fetcher that uses OkHttp 3 as a backend. */
public class CustomNetworkFetcher
extends BaseNetworkFetcher<CustomNetworkFetcher.OkHttpNetworkFetchState> {

public static class OkHttpNetworkFetchState extends FetchState {

public long submitTime;
public long responseTime;
public long fetchCompleteTime;

public OkHttpNetworkFetchState(
Consumer<EncodedImage> consumer, ProducerContext producerContext) {
super(consumer, producerContext);
}
}

private static final String QUEUE_TIME = "queue_time";
private static final String FETCH_TIME = "fetch_time";
private static final String TOTAL_TIME = "total_time";
private static final String IMAGE_SIZE = "image_size";

private final Call.Factory mCallFactory;
private final CacheControl mCacheControl;

private Executor mCancellationExecutor;

/** @param okHttpClient client to use */
public CustomNetworkFetcher(OkHttpClient okHttpClient) {
this(okHttpClient, okHttpClient.dispatcher().executorService());
}

/**
* @param callFactory custom {@link Call.Factory} for fetching image from the network
* @param cancellationExecutor executor on which fetching cancellation is performed if
* cancellation is requested from the UI Thread
*/
public CustomNetworkFetcher(Call.Factory callFactory, Executor cancellationExecutor) {
this(callFactory, cancellationExecutor, true);
}

/**
* @param callFactory custom {@link Call.Factory} for fetching image from the network
* @param cancellationExecutor executor on which fetching cancellation is performed if
* cancellation is requested from the UI Thread
* @param disableOkHttpCache true if network requests should not be cached by OkHttp
*/
public CustomNetworkFetcher(
Call.Factory callFactory, Executor cancellationExecutor, boolean disableOkHttpCache) {
mCallFactory = callFactory;
mCancellationExecutor = cancellationExecutor;
mCacheControl = disableOkHttpCache ? new CacheControl.Builder().noStore().build() : null;
}

@Override
public OkHttpNetworkFetchState createFetchState(
Consumer<EncodedImage> consumer, ProducerContext context) {
return new OkHttpNetworkFetchState(consumer, context);
}

@Override
public void fetch(
final OkHttpNetworkFetchState fetchState, final NetworkFetcher.Callback callback) {
fetchState.submitTime = SystemClock.elapsedRealtime();
final Uri uri = fetchState.getUri();

try {
final Request.Builder requestBuilder = new Request.Builder().url(uri.toString()).get();

if (mCacheControl != null) {
requestBuilder.cacheControl(mCacheControl);
}

final BytesRange bytesRange = fetchState.getContext().getImageRequest().getBytesRange();
if (bytesRange != null) {
requestBuilder.addHeader("Range", bytesRange.toHttpRangeHeaderValue());
}

fetchWithRequest(fetchState, callback, requestBuilder.build());
} catch (Exception e) {
// handle error while creating the request
callback.onFailure(e);
}
}

@Override
public void onFetchCompletion(OkHttpNetworkFetchState fetchState, int byteSize) {
fetchState.fetchCompleteTime = SystemClock.elapsedRealtime();
}

@Override
public Map<String, String> getExtraMap(OkHttpNetworkFetchState fetchState, int byteSize) {
Map<String, String> extraMap = new HashMap<>(4);
extraMap.put(QUEUE_TIME, Long.toString(fetchState.responseTime - fetchState.submitTime));
extraMap.put(FETCH_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.responseTime));
extraMap.put(TOTAL_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.submitTime));
extraMap.put(IMAGE_SIZE, Integer.toString(byteSize));
return extraMap;
}

protected void fetchWithRequest(
final OkHttpNetworkFetchState fetchState,
final NetworkFetcher.Callback callback,
final Request request) {
final Call call = mCallFactory.newCall(request);

fetchState
.getContext()
.addCallbacks(
new BaseProducerContextCallbacks() {
@Override
public void onCancellationRequested() {
if (Looper.myLooper() != Looper.getMainLooper()) {
call.cancel();
} else {
mCancellationExecutor.execute(
new Runnable() {
@Override
public void run() {
call.cancel();
}
});
}
}
});

call.enqueue(
new okhttp3.Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
fetchState.responseTime = SystemClock.elapsedRealtime();
final ResponseBody body = response.body();
try {
if (!response.isSuccessful()) {
handleException(
call, new IOException("Unexpected HTTP code " + response), callback);
return;
}

BytesRange responseRange =
BytesRange.fromContentRangeHeader(response.header("Content-Range"));
if (responseRange != null
&& !(responseRange.from == 0
&& responseRange.to == BytesRange.TO_END_OF_CONTENT)) {
// Only treat as a partial image if the range is not all of the content
fetchState.setResponseBytesRange(responseRange);
fetchState.setOnNewResultStatusFlags(Consumer.IS_PARTIAL_RESULT);
}

long contentLength = body.contentLength();
if (contentLength < 0) {
contentLength = 0;
}
callback.onResponse(body.byteStream(), (int) contentLength);
} catch (Exception e) {
handleException(call, e, callback);
} finally {
body.close();
}
}

@Override
public void onFailure(Call call, IOException e) {
handleException(call, e, callback);
}
});
}

/**
* Handles exceptions.
*
* <p>OkHttp notifies callers of cancellations via an IOException. If IOException is caught after
* request cancellation, then the exception is interpreted as successful cancellation and
* onCancellation is called. Otherwise onFailure is called.
*/
private void handleException(final Call call, final Exception e, final Callback callback) {
if (call.isCanceled()) {
callback.onCancellation();
} else {
callback.onFailure(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ private static OkHttpClient createClient() {
.addInterceptor(new CommonHeaderRequestInterceptor());

if(ConfigUtils.isBetaFlavour()){
builder.sslSocketFactory(SslUtils.INSTANCE.getSslContextForCertificateFile(CommonsApplication.getInstance(), "*.wikimedia.beta.wmflabs.org.cer").getSocketFactory());
builder.sslSocketFactory(SslUtils.INSTANCE.getTrustAllHostsSSLSocketFactory());
}
return builder.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public OkHttpClient provideOkHttpClient(Context context,
.cache(new Cache(dir, OK_HTTP_CACHE_SIZE));

if(ConfigUtils.isBetaFlavour()){
builder.sslSocketFactory(SslUtils.INSTANCE.getSslContextForCertificateFile(context, "*.wikimedia.beta.wmflabs.org.cer").getSocketFactory());
builder.sslSocketFactory(SslUtils.INSTANCE.getTrustAllHostsSSLSocketFactory());
}
return builder.build();
}
Expand Down
51 changes: 4 additions & 47 deletions app/src/main/java/fr/free/nrw/commons/di/SslUtils.kt
Original file line number Diff line number Diff line change
@@ -1,59 +1,16 @@
package fr.free.nrw.commons.di

import android.content.Context
import android.util.Log
import java.security.KeyManagementException
import java.security.KeyStore
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
import java.security.cert.Certificate
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import javax.net.ssl.*
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager

object SslUtils {

fun getSslContextForCertificateFile(context: Context, fileName: String): SSLContext {
try {
val keyStore = SslUtils.getKeyStore(context, fileName)
val sslContext = SSLContext.getInstance("SSL")
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(keyStore)
sslContext.init(null, trustManagerFactory.trustManagers, SecureRandom())
return sslContext
} catch (e: Exception) {
val msg = "Error during creating SslContext for certificate from assets"
e.printStackTrace()
throw RuntimeException(msg)
}
}

private fun getKeyStore(context: Context, fileName: String): KeyStore? {
var keyStore: KeyStore? = null
try {
val assetManager = context.assets
val cf = CertificateFactory.getInstance("X.509")
val caInput = assetManager.open(fileName)
val ca: Certificate
try {
ca = cf.generateCertificate(caInput)
Log.d("SslUtilsAndroid", "ca=" + (ca as X509Certificate).subjectDN)
} finally {
caInput.close()
}

val keyStoreType = KeyStore.getDefaultType()
keyStore = KeyStore.getInstance(keyStoreType)
keyStore!!.load(null, null)
keyStore.setCertificateEntry("ca", ca)
} catch (e: Exception) {
e.printStackTrace()
}

return keyStore
}

fun getTrustAllHostsSSLSocketFactory(): SSLSocketFactory? {
try {
// Create a trust manager that does not validate certificate chains
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package fr.free.nrw.commons

import android.app.Application
import android.content.ContentProviderClient
import android.content.Context
import androidx.collection.LruCache
Expand All @@ -14,7 +15,7 @@ import fr.free.nrw.commons.di.DaggerCommonsApplicationComponent
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.location.LocationServiceManager

class TestCommonsApplication : CommonsApplication() {
class TestCommonsApplication : Application() {
private var mockApplicationComponent: CommonsApplicationComponent? = null

override fun onCreate() {
Expand All @@ -25,9 +26,6 @@ class TestCommonsApplication : CommonsApplication() {
}
super.onCreate()
}

// No leakcanary in unit tests.
override fun setupLeakCanary(): RefWatcher = RefWatcher.DISABLED
}

@Suppress("MemberVisibilityCanBePrivate")
Expand Down

0 comments on commit fa87eb5

Please sign in to comment.