From fa87eb566128c777dde3afc79ba2b68ce70efd55 Mon Sep 17 00:00:00 2001 From: Ashish Kumar Date: Mon, 27 Jan 2020 10:43:18 +0530 Subject: [PATCH] Fixes #3345 (#3350) * Fixes #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 --- .../assets/*.wikimedia.beta.wmflabs.org.cer | Bin 1870 -> 0 bytes .../free/nrw/commons/CommonsApplication.java | 20 +- .../nrw/commons/CustomNetworkFetcher.java | 206 ++++++++++++++++++ .../nrw/commons/OkHttpConnectionFactory.java | 2 +- .../free/nrw/commons/di/NetworkingModule.java | 2 +- .../java/fr/free/nrw/commons/di/SslUtils.kt | 51 +---- .../nrw/commons/TestCommonsApplication.kt | 6 +- 7 files changed, 231 insertions(+), 56 deletions(-) delete mode 100644 app/src/main/assets/*.wikimedia.beta.wmflabs.org.cer create mode 100644 app/src/main/java/fr/free/nrw/commons/CustomNetworkFetcher.java diff --git a/app/src/main/assets/*.wikimedia.beta.wmflabs.org.cer b/app/src/main/assets/*.wikimedia.beta.wmflabs.org.cer deleted file mode 100644 index ffb4c37c991824ee2b3b563ee34202355eb0cbe8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1870 zcmXqLV)rs=Vl!I6%*4pVB*Yxpkasq{;iy4Tn#jz_UsmfrGT>$7)N1o+`_9YA$j!=N z;AO~dz{$oO%EBhh6dG(OW*`FMa0&DJq?V``E4bz*7gZLN7%Cemg2b7HrBK8b97{_w z@{2M{DitD(4dldm4J{1~4J`~T3=B*Sqr`cQ3=E76&7fQZbwgDHWw0?kGFp1&nc11S zsVSL>dP%7ziF)O^X*r2W#d`Tg=?0Mo!ffn7SDv$FVr1h0Gng6KnVlF|mcQ7@WHC*@ zVa=7y@X5ZjPUz~_X7%&2dQD${?$_)@R+F4(r$$%eXDocNxQSKFpovw;fDh;nS$;;w|12!bOzaH?vLL=H3&{T*+H8!htnAD{tD!6= z10ImHFi1I*0Ru!aABz}^$imfIF5mQWUYYK=tJvlK#2yFd_yPlYkYZ&P2?MbPkriH9 z$t<;ZcRjmkxqX>y>if?t-Yhi8hndXC$dYW3XrKq<8!)yhWt5Z@Sn2EMCl?p!W#*OW zRv7E$q?Qz?LSh0OKKgL=Ol`_g^~tG4B{&Mz%W#-cz0rl2gfs2G_1 z@EB2+UzwPW#R!-kC7JnoiA7jUfTmF#A%r&tH%UX%H}=qhYQ+&YP+1%y0}Oaj#=#XX zz<32o;|vw3c3fcsk;WM!iduSAsYUr(Fi?Z#5hg}4DOesU zD8|T3O^mE;Twtx&Dw$ZASQ@{vG=66I$WX?>zCA|zX>G>;j~kB3DvKW9^(?$5e9HE$ zcY6}8q%torl3`$A%&Xn!;>WxB>0$DRB~!5~^mB ze&tv6xwE~?bCSHxUf*x2Rx?Ao4z1(<2vqVqM`@exYvJ{*+Hz;umc;h@I!4SXvLM}G+wVcXcYr<6C)!-@cx)JRmB>z)zSK=vX5-~^=D(zmg~29<<}R7q}bdMbzv`D z{#k?L)3Jw=e@e`LJy)HO`A4;3o4MeFQ&YqrOV~Y3{J-(Obl%E#waEVCci;3yhON2r zlr=eSCePj@hi%p@Vn4IC_4Ml%`ZEG1C7zue|6}7#*P>;W6)`^+HNTor8JxU4@mxm# zLyIMj(_*40m!Dr>)?(zqdGsRl%J*+9c@}JNwleYKc`lm%eCpMnRr(8e|DC3C+1;T( z{2I5;t+MCQYo^_5-#0_@$K5hRsnS>rH^T|H7dhm8&J&+0!Ro&Ghg#SktJAJkTh|{s f^*4KWMdhcdGo)&4xx4Nq*!P{>v@Ugp{K66dVOyg> diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java index 3c9398babd..e5b124209b 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -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; @@ -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; @@ -83,6 +88,9 @@ public class CommonsApplication extends Application { @Inject @Named("default_preferences") JsonKvStore defaultPrefs; + @Inject + OkHttpClient okHttpClient; + /** * Constants begin */ @@ -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) { diff --git a/app/src/main/java/fr/free/nrw/commons/CustomNetworkFetcher.java b/app/src/main/java/fr/free/nrw/commons/CustomNetworkFetcher.java new file mode 100644 index 0000000000..4879b143be --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/CustomNetworkFetcher.java @@ -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 { + + public static class OkHttpNetworkFetchState extends FetchState { + + public long submitTime; + public long responseTime; + public long fetchCompleteTime; + + public OkHttpNetworkFetchState( + Consumer 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 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 getExtraMap(OkHttpNetworkFetchState fetchState, int byteSize) { + Map 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. + * + *

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); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/OkHttpConnectionFactory.java b/app/src/main/java/fr/free/nrw/commons/OkHttpConnectionFactory.java index 3e7f973031..8b12ee5f0f 100644 --- a/app/src/main/java/fr/free/nrw/commons/OkHttpConnectionFactory.java +++ b/app/src/main/java/fr/free/nrw/commons/OkHttpConnectionFactory.java @@ -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(); } diff --git a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java index 581ff37fd1..830b142024 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java @@ -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(); } diff --git a/app/src/main/java/fr/free/nrw/commons/di/SslUtils.kt b/app/src/main/java/fr/free/nrw/commons/di/SslUtils.kt index a24c6ddf91..043cdbc609 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/SslUtils.kt +++ b/app/src/main/java/fr/free/nrw/commons/di/SslUtils.kt @@ -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 diff --git a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt index 5368a34ad0..a59748f1d8 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt @@ -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 @@ -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() { @@ -25,9 +26,6 @@ class TestCommonsApplication : CommonsApplication() { } super.onCreate() } - - // No leakcanary in unit tests. - override fun setupLeakCanary(): RefWatcher = RefWatcher.DISABLED } @Suppress("MemberVisibilityCanBePrivate")