Skip to content

Fixes #3345 #3350

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

Merged
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
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