diff --git a/AnkiDroid/build.gradle b/AnkiDroid/build.gradle
index 7c677c6e3439..dc0819d53408 100644
--- a/AnkiDroid/build.gradle
+++ b/AnkiDroid/build.gradle
@@ -57,7 +57,6 @@ android {
buildConfigField "String", "ACRA_URL", '"https://ankidroid.org/acra/report"'
}
}
- useLibrary 'org.apache.http.legacy'
testOptions {
animationsDisabled true
diff --git a/AnkiDroid/src/main/AndroidManifest.xml b/AnkiDroid/src/main/AndroidManifest.xml
index 3e1484f1cb05..bc7ed1622d9a 100644
--- a/AnkiDroid/src/main/AndroidManifest.xml
+++ b/AnkiDroid/src/main/AndroidManifest.xml
@@ -382,9 +382,6 @@
-
{
@@ -199,12 +200,11 @@ private Payload doOneInBackground(Payload data) {
}
- @SuppressWarnings("deprecation") // tracking HTTP transport change in github already
private Payload doInBackgroundLogin(Payload data) {
String username = (String) data.data[0];
String password = (String) data.data[1];
HttpSyncer server = new RemoteServer(this, null);
- org.apache.http.HttpResponse ret;
+ Response ret;
try {
ret = server.hostKey(username, password);
} catch (UnknownHttpResponseException e) {
@@ -223,16 +223,16 @@ private Payload doInBackgroundLogin(Payload data) {
String hostkey = null;
boolean valid = false;
if (ret != null) {
- data.returnType = ret.getStatusLine().getStatusCode();
- Timber.d("doInBackgroundLogin - response from server: %d, (%s)", data.returnType, ret.getStatusLine().getReasonPhrase());
+ data.returnType = ret.code();
+ Timber.d("doInBackgroundLogin - response from server: %d, (%s)", data.returnType, ret.message());
if (data.returnType == 200) {
try {
- JSONObject jo = (new JSONObject(server.stream2String(ret.getEntity().getContent())));
+ JSONObject jo = (new JSONObject(ret.body().string()));
hostkey = jo.getString("key");
valid = (hostkey != null) && (hostkey.length() > 0);
} catch (JSONException e) {
valid = false;
- } catch (IllegalStateException | IOException e) {
+ } catch (IllegalStateException | IOException | NullPointerException e) {
throw new RuntimeException(e);
}
}
diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/sync/CountingFileRequestBody.java b/AnkiDroid/src/main/java/com/ichi2/libanki/sync/CountingFileRequestBody.java
new file mode 100644
index 000000000000..43c4adea86a4
--- /dev/null
+++ b/AnkiDroid/src/main/java/com/ichi2/libanki/sync/CountingFileRequestBody.java
@@ -0,0 +1,76 @@
+/****************************************************************************************
+ * Copyright (c) 2019 Mike Hardy *
+ * *
+ * This program is free software; you can redistribute it and/or modify it under *
+ * the terms of the GNU General Public License as published by the Free Software *
+ * Foundation; either version 3 of the License, or (at your option) any later *
+ * version. *
+ * *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY *
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A *
+ * PARTICULAR PURPOSE. See the GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License along with *
+ * this program. If not, see . *
+ ****************************************************************************************/
+
+package com.ichi2.libanki.sync;
+
+import java.io.File;
+import java.io.IOException;
+
+import okhttp3.MediaType;
+import okhttp3.RequestBody;
+import okhttp3.internal.Util;
+import okio.BufferedSink;
+import okio.Okio;
+import okio.Source;
+
+
+// Note that in current versions of OkHTTP this is unnecessary as they support
+// Decorators / hooks more easily with the builder API, allowing upload transfer tracking
+// without a separate object. I believe we will have to move to API21+ for that to be possible
+public class CountingFileRequestBody extends RequestBody {
+
+ private static final int SEGMENT_SIZE = 2048; // okio.Segment.SIZE
+
+ private final File file;
+ private final ProgressListener listener;
+ private final String contentType;
+
+ public CountingFileRequestBody(File file, String contentType, ProgressListener listener) {
+ this.file = file;
+ this.contentType = contentType;
+ this.listener = listener;
+ }
+
+ @Override
+ public long contentLength() {
+ return file.length();
+ }
+
+ @Override
+ public MediaType contentType() {
+ return MediaType.parse(contentType);
+ }
+
+ @Override
+ public void writeTo(BufferedSink sink) throws IOException {
+ Source source = null;
+ try {
+ source = Okio.source(file);
+ long read;
+
+ while ((read = source.read(sink.buffer(), SEGMENT_SIZE)) != -1) {
+ sink.flush();
+ this.listener.transferred(read);
+ }
+ } finally {
+ Util.closeQuietly(source);
+ }
+ }
+
+ public interface ProgressListener {
+ void transferred(long num);
+ }
+}
\ No newline at end of file
diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/sync/FullSyncer.java b/AnkiDroid/src/main/java/com/ichi2/libanki/sync/FullSyncer.java
index 742bea5f1b60..f44efc50b096 100644
--- a/AnkiDroid/src/main/java/com/ichi2/libanki/sync/FullSyncer.java
+++ b/AnkiDroid/src/main/java/com/ichi2/libanki/sync/FullSyncer.java
@@ -39,6 +39,8 @@
import java.util.HashMap;
import java.util.Locale;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
import timber.log.Timber;
@SuppressWarnings({"PMD.AvoidThrowingRawExceptionTypes","PMD.NPathComplexity"})
@@ -73,19 +75,21 @@ public String syncURL() {
@Override
- @SuppressWarnings("deprecation") // tracking HTTP transport change in github already
public Object[] download() throws UnknownHttpResponseException {
InputStream cont;
+ ResponseBody body = null;
try {
- org.apache.http.HttpResponse ret = super.req("download");
- if (ret == null) {
+ Response ret = super.req("download");
+ if (ret == null || ret.body() == null) {
return null;
}
- cont = ret.getEntity().getContent();
- } catch (IllegalStateException e1) {
+ body = ret.body();
+ cont = body.byteStream();
+ } catch (IllegalArgumentException e1) {
+ if (body != null) {
+ body.close();
+ }
throw new RuntimeException(e1);
- } catch (IOException e1) {
- return null;
}
String path;
if (mCol != null) {
@@ -111,6 +115,8 @@ public Object[] download() throws UnknownHttpResponseException {
} catch (IOException e) {
Timber.e(e, "Full sync failed to download collection.");
return new Object[] { "sdAccessError" };
+ } finally {
+ body.close();
}
// check the received file is ok
@@ -141,7 +147,6 @@ public Object[] download() throws UnknownHttpResponseException {
@Override
- @SuppressWarnings("deprecation") // tracking HTTP transport change in github already
public Object[] upload() throws UnknownHttpResponseException {
// make sure it's ok before we try to upload
mCon.publishProgress(R.string.sync_check_upload_file);
@@ -154,19 +159,19 @@ public Object[] upload() throws UnknownHttpResponseException {
// apply some adjustments, then upload
mCol.beforeUpload();
String filePath = mCol.getPath();
- org.apache.http.HttpResponse ret;
+ Response ret;
mCon.publishProgress(R.string.sync_uploading_message);
try {
ret = super.req("upload", new FileInputStream(filePath));
- if (ret == null) {
+ if (ret == null || ret.body() == null) {
return null;
}
- int status = ret.getStatusLine().getStatusCode();
+ int status = ret.code();
if (status != 200) {
// error occurred
- return new Object[] { "error", status, ret.getStatusLine().getReasonPhrase() };
+ return new Object[] { "error", status, ret.message() };
} else {
- return new Object[] { super.stream2String(ret.getEntity().getContent()) };
+ return new Object[] { ret.body().string() };
}
} catch (IllegalStateException | IOException e) {
throw new RuntimeException(e);
diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/sync/HttpSyncer.java b/AnkiDroid/src/main/java/com/ichi2/libanki/sync/HttpSyncer.java
index ba864ed279f9..68d726d7a034 100644
--- a/AnkiDroid/src/main/java/com/ichi2/libanki/sync/HttpSyncer.java
+++ b/AnkiDroid/src/main/java/com/ichi2/libanki/sync/HttpSyncer.java
@@ -1,7 +1,8 @@
/***************************************************************************************
* Copyright (c) 2012 Norbert Nagold *
* Copyright (c) 2012 Kostas Spyropoulos *
- * Copyright (c) 2014 Timothy Rae
+ * Copyright (c) 2014 Timothy Rae *
+ * Copyright (c) 2019 Mike Hardy *
* *
* This program is free software; you can redistribute it and/or modify it under *
* the terms of the GNU General Public License as published by the Free Software *
@@ -19,7 +20,6 @@
package com.ichi2.libanki.sync;
-
import android.content.SharedPreferences;
import android.net.Uri;
@@ -30,7 +30,7 @@
import com.ichi2.libanki.Utils;
import com.ichi2.utils.VersionUtils;
-import org.apache.commons.httpclient.contrib.ssl.EasySSLSocketFactory;
+import org.apache.http.entity.AbstractHttpEntity;
import org.json.JSONException;
import org.json.JSONObject;
@@ -52,10 +52,15 @@
import java.util.Locale;
import java.util.Map;
import java.util.Random;
+import java.util.concurrent.TimeUnit;
import java.util.zip.GZIPOutputStream;
import javax.net.ssl.SSLException;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
import timber.log.Timber;
/**
@@ -65,11 +70,12 @@
* - 502: ankiweb down
* - 503/504: server too busy
*/
-@SuppressWarnings({"PMD.AvoidThrowingRawExceptionTypes","PMD.NPathComplexity",
- "deprecation"}) // tracking HTTP transport change in github already
+@SuppressWarnings( {"PMD.AvoidThrowingRawExceptionTypes", "PMD.NPathComplexity"})
public class HttpSyncer {
private static final String BOUNDARY = "Anki-sync-boundary";
+ private static final MediaType ANKI_POST_TYPE = MediaType.get("multipart/form-data; boundary=" + BOUNDARY);
+
public static final String ANKIWEB_STATUS_OK = "OK";
public volatile long bytesSent = 0;
@@ -95,40 +101,40 @@ public HttpSyncer(String hkey, Connection con) {
}
- public void assertOk(org.apache.http.HttpResponse resp) throws UnknownHttpResponseException {
+ public void assertOk(Response resp) throws UnknownHttpResponseException {
// Throw RuntimeException if HTTP error
if (resp == null) {
throw new UnknownHttpResponseException("Null HttpResponse", -2);
}
- int resultCode = resp.getStatusLine().getStatusCode();
+ int resultCode = resp.code();
if (!(resultCode == 200 || resultCode == 403)) {
- String reason = resp.getStatusLine().getReasonPhrase();
+ String reason = resp.message();
throw new UnknownHttpResponseException(reason, resultCode);
}
}
- public org.apache.http.HttpResponse req(String method) throws UnknownHttpResponseException {
+ public Response req(String method) throws UnknownHttpResponseException {
return req(method, null);
}
- public org.apache.http.HttpResponse req(String method, InputStream fobj) throws UnknownHttpResponseException {
+ public Response req(String method, InputStream fobj) throws UnknownHttpResponseException {
return req(method, fobj, 6);
}
- public org.apache.http.HttpResponse req(String method, int comp, InputStream fobj) throws UnknownHttpResponseException {
+ public Response req(String method, int comp, InputStream fobj) throws UnknownHttpResponseException {
return req(method, fobj, comp);
}
- public org.apache.http.HttpResponse req(String method, InputStream fobj, int comp) throws UnknownHttpResponseException {
+ public Response req(String method, InputStream fobj, int comp) throws UnknownHttpResponseException {
return req(method, fobj, comp, null);
}
- private org.apache.http.HttpResponse req(String method, InputStream fobj, int comp, JSONObject registerData) throws UnknownHttpResponseException {
+ private Response req(String method, InputStream fobj, int comp, JSONObject registerData) throws UnknownHttpResponseException {
File tmpFileBuffer = null;
try {
String bdry = "--" + BOUNDARY;
@@ -185,33 +191,40 @@ private org.apache.http.HttpResponse req(String method, InputStream fobj, int co
} else {
url = syncURL() + method;
}
- org.apache.http.client.methods.HttpPost httpPost = new org.apache.http.client.methods.HttpPost(url);
- org.apache.http.HttpEntity entity = new ProgressByteEntity(tmpFileBuffer);
-
- // body
- httpPost.setEntity(entity);
- httpPost.setHeader("Content-type", "multipart/form-data; boundary=" + BOUNDARY);
-
- // HttpParams
- org.apache.http.params.HttpParams params = new org.apache.http.params.BasicHttpParams();
- params.setParameter(org.apache.http.conn.params.ConnManagerPNames.MAX_TOTAL_CONNECTIONS, 30);
- params.setParameter(org.apache.http.conn.params.ConnManagerPNames.MAX_CONNECTIONS_PER_ROUTE, new org.apache.http.conn.params.ConnPerRouteBean(30));
- params.setParameter(org.apache.http.params.CoreProtocolPNames.USE_EXPECT_CONTINUE, false);
- params.setParameter(org.apache.http.params.CoreProtocolPNames.USER_AGENT, "AnkiDroid-" + VersionUtils.getPkgVersionName());
- org.apache.http.params.HttpProtocolParams.setVersion(params, org.apache.http.HttpVersion.HTTP_1_1);
- org.apache.http.params.HttpConnectionParams.setSoTimeout(params, Connection.CONN_TIMEOUT);
-
- // Registry
- org.apache.http.conn.scheme.SchemeRegistry registry = new org.apache.http.conn.scheme.SchemeRegistry();
- registry.register(new org.apache.http.conn.scheme.Scheme("http", org.apache.http.conn.scheme.PlainSocketFactory.getSocketFactory(), 80));
- registry.register(new org.apache.http.conn.scheme.Scheme("https", new EasySSLSocketFactory(), 443));
- org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager cm = new org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager(params, registry);
+
+ Request.Builder requestBuilder = new Request.Builder();
+ requestBuilder.url(url);
+
+ requestBuilder.post(new CountingFileRequestBody(tmpFileBuffer, ANKI_POST_TYPE.toString(), num -> {
+ bytesSent += num;
+ publishProgress();
+ }));
+ Request httpPost = requestBuilder.build();
try {
- org.apache.http.client.HttpClient httpClient = new org.apache.http.impl.client.DefaultHttpClient(cm, params);
- org.apache.http.HttpResponse httpResponse = httpClient.execute(httpPost);
+ OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder().addNetworkInterceptor(chain -> chain.proceed(
+ chain.request()
+ .newBuilder()
+ .header("User-Agent", "AnkiDroid-" + VersionUtils.getPkgVersionName())
+ .build()
+ ));
+ Tls12SocketFactory.enableTls12OnPreLollipop(clientBuilder)
+ .followRedirects(true)
+ .followSslRedirects(true)
+ .retryOnConnectionFailure(true)
+ .cache(null)
+ .connectTimeout(Connection.CONN_TIMEOUT, TimeUnit.SECONDS)
+ .writeTimeout(Connection.CONN_TIMEOUT, TimeUnit.SECONDS)
+ .readTimeout(Connection.CONN_TIMEOUT, TimeUnit.SECONDS);
+ OkHttpClient httpClient = clientBuilder.build();
+ Response httpResponse = httpClient.newCall(httpPost).execute();
+
// we assume badAuthRaises flag from Anki Desktop always False
// so just throw new RuntimeException if response code not 200 or 403
+ Timber.d("TLSVersion in use is: %s",
+ (httpResponse.handshake() != null ? httpResponse.handshake().tlsVersion() : "unknown"));
+
+
assertOk(httpResponse);
return httpResponse;
} catch (SSLException e) {
@@ -231,6 +244,7 @@ private org.apache.http.HttpResponse req(String method, InputStream fobj, int co
}
+ // Could be replaced by Compat copy method if that method took listener for bytesReceived/publishProgress()
public void writeToFile(InputStream source, String destination) throws IOException {
File file = new File(destination);
OutputStream output = null;
@@ -259,11 +273,6 @@ public void writeToFile(InputStream source, String destination) throws IOExcepti
}
- public String stream2String(InputStream stream) {
- return stream2String(stream, -1);
- }
-
-
public String stream2String(InputStream stream, int maxSize) {
BufferedReader rd;
try {
@@ -285,9 +294,11 @@ public String stream2String(InputStream stream, int maxSize) {
private void publishProgress() {
+ Timber.d("Publishing progress");
if (mCon != null && (mNextSendR <= bytesReceived || mNextSendS <= bytesSent)) {
long bR = bytesReceived;
long bS = bytesSent;
+ Timber.d("Current progress: %d, %d", bytesReceived, bytesSent);
mNextSendR = (bR / 1024 + 1) * 1024;
mNextSendS = (bS / 1024 + 1) * 1024;
mCon.publishProgress(0, bS, bR);
@@ -295,7 +306,7 @@ private void publishProgress() {
}
- public org.apache.http.HttpResponse hostKey(String arg1, String arg2) throws UnknownHttpResponseException {
+ public Response hostKey(String arg1, String arg2) throws UnknownHttpResponseException {
return null;
}
@@ -319,12 +330,13 @@ public long finish() throws UnknownHttpResponseException {
return 0;
}
+
public void abort() throws UnknownHttpResponseException {
// do nothing
}
- public org.apache.http.HttpResponse meta() throws UnknownHttpResponseException {
+ public Response meta() throws UnknownHttpResponseException {
return null;
}
@@ -348,7 +360,8 @@ public void applyChunk(JSONObject sech) throws UnknownHttpResponseException {
// do nothing
}
- public class ProgressByteEntity extends org.apache.http.entity.AbstractHttpEntity {
+
+ public class ProgressByteEntity extends AbstractHttpEntity {
private InputStream mInputStream;
private long mLength;
@@ -420,7 +433,7 @@ public static ByteArrayInputStream getInputStream(String string) {
public String syncURL() {
// Allow user to specify custom sync server
SharedPreferences userPreferences = AnkiDroidApp.getSharedPrefs(AnkiDroidApp.getInstance());
- if (userPreferences!= null && userPreferences.getBoolean("useCustomSyncServer", false)) {
+ if (userPreferences != null && userPreferences.getBoolean("useCustomSyncServer", false)) {
Uri syncBase = Uri.parse(userPreferences.getString("syncBaseUrl", Consts.SYNC_BASE));
return syncBase.buildUpon().appendPath("sync").toString() + "/";
}
diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/sync/RemoteMediaServer.java b/AnkiDroid/src/main/java/com/ichi2/libanki/sync/RemoteMediaServer.java
index fb93fd3e41b4..d8780fc2162b 100644
--- a/AnkiDroid/src/main/java/com/ichi2/libanki/sync/RemoteMediaServer.java
+++ b/AnkiDroid/src/main/java/com/ichi2/libanki/sync/RemoteMediaServer.java
@@ -42,10 +42,10 @@
import java.util.Locale;
import java.util.zip.ZipFile;
+import okhttp3.Response;
import timber.log.Timber;
-@SuppressWarnings({"PMD.AvoidThrowingRawExceptionTypes","PMD.MethodNamingConventions",
- "deprecation"}) // tracking HTTP transport change in github already
+@SuppressWarnings({"PMD.AvoidThrowingRawExceptionTypes","PMD.MethodNamingConventions"})
public class RemoteMediaServer extends HttpSyncer {
private Collection mCol;
@@ -77,8 +77,8 @@ public JSONObject begin() throws UnknownHttpResponseException, MediaSyncExceptio
mPostVars.put("v",
String.format(Locale.US, "ankidroid,%s,%s", VersionUtils.getPkgVersionName(), Utils.platDesc()));
- org.apache.http.HttpResponse resp = super.req("begin", super.getInputStream(Utils.jsonToString(new JSONObject())));
- JSONObject jresp = new JSONObject(super.stream2String(resp.getEntity().getContent()));
+ Response resp = super.req("begin", HttpSyncer.getInputStream(Utils.jsonToString(new JSONObject())));
+ JSONObject jresp = new JSONObject(resp.body().string());
JSONObject ret = _dataOnly(jresp, JSONObject.class);
mSKey = ret.getString("sk");
return ret;
@@ -94,9 +94,9 @@ public JSONArray mediaChanges(int lastUsn) throws UnknownHttpResponseException,
mPostVars = new HashMap<>();
mPostVars.put("sk", mSKey);
- org.apache.http.HttpResponse resp = super.req("mediaChanges",
- super.getInputStream(Utils.jsonToString(new JSONObject().put("lastUsn", lastUsn))));
- JSONObject jresp = new JSONObject(super.stream2String(resp.getEntity().getContent()));
+ Response resp = super.req("mediaChanges",
+ HttpSyncer.getInputStream(Utils.jsonToString(new JSONObject().put("lastUsn", lastUsn))));
+ JSONObject jresp = new JSONObject(resp.body().string());
return _dataOnly(jresp, JSONArray.class);
} catch (JSONException | IOException e) {
throw new RuntimeException(e);
@@ -111,19 +111,23 @@ public JSONArray mediaChanges(int lastUsn) throws UnknownHttpResponseException,
* be automatically deleted when the stream is closed.
*/
public ZipFile downloadFiles(List top) throws UnknownHttpResponseException {
+ Response resp = null;
try {
- org.apache.http.HttpResponse resp;
resp = super.req("downloadFiles",
- super.getInputStream(Utils.jsonToString(new JSONObject().put("files", new JSONArray(top)))));
+ HttpSyncer.getInputStream(Utils.jsonToString(new JSONObject().put("files", new JSONArray(top)))));
String zipPath = mCol.getPath().replaceFirst("collection\\.anki2$", "tmpSyncFromServer.zip");
// retrieve contents and save to file on disk:
- super.writeToFile(resp.getEntity().getContent(), zipPath);
+ super.writeToFile(resp.body().byteStream(), zipPath);
return new ZipFile(new File(zipPath), ZipFile.OPEN_READ | ZipFile.OPEN_DELETE);
} catch (JSONException e) {
throw new RuntimeException(e);
- } catch (IOException e) {
+ } catch (IOException | NullPointerException e) {
Timber.e(e, "Failed to download requested media files");
throw new RuntimeException(e);
+ } finally {
+ if (resp != null && resp.body() != null) {
+ resp.body().close();
+ }
}
}
@@ -131,10 +135,10 @@ public ZipFile downloadFiles(List top) throws UnknownHttpResponseExcepti
public JSONArray uploadChanges(File zip) throws UnknownHttpResponseException, MediaSyncException {
try {
// no compression, as we compress the zip file instead
- org.apache.http.HttpResponse resp = super.req("uploadChanges", new FileInputStream(zip), 0);
- JSONObject jresp = new JSONObject(super.stream2String(resp.getEntity().getContent()));
+ Response resp = super.req("uploadChanges", new FileInputStream(zip), 0);
+ JSONObject jresp = new JSONObject(resp.body().string());
return _dataOnly(jresp, JSONArray.class);
- } catch (JSONException | IOException e) {
+ } catch (JSONException | IOException | NullPointerException e) {
throw new RuntimeException(e);
}
}
@@ -143,11 +147,11 @@ public JSONArray uploadChanges(File zip) throws UnknownHttpResponseException, Me
// args: local
public String mediaSanity(int lcnt) throws UnknownHttpResponseException, MediaSyncException {
try {
- org.apache.http.HttpResponse resp = super.req("mediaSanity",
- super.getInputStream(Utils.jsonToString(new JSONObject().put("local", lcnt))));
- JSONObject jresp = new JSONObject(super.stream2String(resp.getEntity().getContent()));
+ Response resp = super.req("mediaSanity",
+ HttpSyncer.getInputStream(Utils.jsonToString(new JSONObject().put("local", lcnt))));
+ JSONObject jresp = new JSONObject(resp.body().string());
return _dataOnly(jresp, String.class);
- } catch (JSONException | IOException e) {
+ } catch (JSONException | IOException | NullPointerException e) {
throw new RuntimeException(e);
}
}
diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/sync/RemoteServer.java b/AnkiDroid/src/main/java/com/ichi2/libanki/sync/RemoteServer.java
index 06889a7a5ba3..b6d92d78994d 100644
--- a/AnkiDroid/src/main/java/com/ichi2/libanki/sync/RemoteServer.java
+++ b/AnkiDroid/src/main/java/com/ichi2/libanki/sync/RemoteServer.java
@@ -30,8 +30,9 @@
import java.util.HashMap;
import java.util.Locale;
-@SuppressWarnings({"PMD.AvoidThrowingRawExceptionTypes","PMD.MethodNamingConventions",
- "deprecation"}) // tracking HTTP transport change in github already
+import okhttp3.Response;
+
+@SuppressWarnings({"PMD.AvoidThrowingRawExceptionTypes","PMD.MethodNamingConventions"})
public class RemoteServer extends HttpSyncer {
public RemoteServer(Connection con, String hkey) {
@@ -41,13 +42,13 @@ public RemoteServer(Connection con, String hkey) {
/** Returns hkey or none if user/pw incorrect. */
@Override
- public org.apache.http.HttpResponse hostKey(String user, String pw) throws UnknownHttpResponseException {
+ public Response hostKey(String user, String pw) throws UnknownHttpResponseException {
try {
mPostVars = new HashMap<>();
JSONObject jo = new JSONObject();
jo.put("u", user);
jo.put("p", pw);
- return super.req("hostKey", super.getInputStream(Utils.jsonToString(jo)));
+ return super.req("hostKey", HttpSyncer.getInputStream(Utils.jsonToString(jo)));
} catch (JSONException e) {
return null;
}
@@ -55,7 +56,7 @@ public org.apache.http.HttpResponse hostKey(String user, String pw) throws Unkno
@Override
- public org.apache.http.HttpResponse meta() throws UnknownHttpResponseException {
+ public Response meta() throws UnknownHttpResponseException {
try {
mPostVars = new HashMap<>();
mPostVars.put("k", mHKey);
@@ -64,7 +65,7 @@ public org.apache.http.HttpResponse meta() throws UnknownHttpResponseException {
jo.put("v", Consts.SYNC_VER);
jo.put("cv",
String.format(Locale.US, "ankidroid,%s,%s", VersionUtils.getPkgVersionName(), Utils.platDesc()));
- return super.req("meta", super.getInputStream(Utils.jsonToString(jo)));
+ return super.req("meta", HttpSyncer.getInputStream(Utils.jsonToString(jo)));
} catch (JSONException e) {
throw new RuntimeException(e);
}
@@ -113,9 +114,9 @@ public void abort() throws UnknownHttpResponseException {
/** Python has dynamic type deduction, but we don't, so return String **/
private String _run(String cmd, JSONObject data) throws UnknownHttpResponseException {
- org.apache.http.HttpResponse ret = super.req(cmd, super.getInputStream(Utils.jsonToString(data)));
+ Response ret = super.req(cmd, HttpSyncer.getInputStream(Utils.jsonToString(data)));
try {
- return super.stream2String(ret.getEntity().getContent());
+ return ret.body().string();
} catch (IllegalStateException | IOException e) {
throw new RuntimeException(e);
}
diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/sync/Syncer.java b/AnkiDroid/src/main/java/com/ichi2/libanki/sync/Syncer.java
index 268841c21166..f4195ceb9026 100644
--- a/AnkiDroid/src/main/java/com/ichi2/libanki/sync/Syncer.java
+++ b/AnkiDroid/src/main/java/com/ichi2/libanki/sync/Syncer.java
@@ -42,6 +42,7 @@
import java.util.Locale;
import java.util.Map;
+import okhttp3.Response;
import timber.log.Timber;
@SuppressWarnings({"deprecation", // tracking HTTP transport change in github already
@@ -84,17 +85,16 @@ public Object[] sync() throws UnknownHttpResponseException {
}
- @SuppressWarnings("deprecation") // tracking HTTP transport change in github already
public Object[] sync(Connection con) throws UnknownHttpResponseException {
mSyncMsg = "";
// if the deck has any pending changes, flush them first and bump mod time
mCol.save();
// step 1: login & metadata
- org.apache.http.HttpResponse ret = mServer.meta();
+ Response ret = mServer.meta();
if (ret == null) {
return null;
}
- int returntype = ret.getStatusLine().getStatusCode();
+ int returntype = ret.code();
if (returntype == 403) {
return new Object[] { "badAuth" };
}
@@ -102,7 +102,7 @@ public Object[] sync(Connection con) throws UnknownHttpResponseException {
mCol.getDb().getDatabase().beginTransaction();
try {
Timber.i("Sync: getting meta data from server");
- JSONObject rMeta = new JSONObject(mServer.stream2String(ret.getEntity().getContent()));
+ JSONObject rMeta = new JSONObject(ret.body().string());
mCol.log("rmeta", rMeta);
mSyncMsg = rMeta.getString("msg");
if (!rMeta.getBoolean("cont")) {
diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/sync/Tls12SocketFactory.java b/AnkiDroid/src/main/java/com/ichi2/libanki/sync/Tls12SocketFactory.java
new file mode 100644
index 000000000000..144c564cd105
--- /dev/null
+++ b/AnkiDroid/src/main/java/com/ichi2/libanki/sync/Tls12SocketFactory.java
@@ -0,0 +1,158 @@
+/****************************************************************************************
+ * Copyright (c) 2019 Mike Hardy *
+ * *
+ * This program is free software; you can redistribute it and/or modify it under *
+ * the terms of the GNU General Public License as published by the Free Software *
+ * Foundation; either version 3 of the License, or (at your option) any later *
+ * version. *
+ * *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY *
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A *
+ * PARTICULAR PURPOSE. See the GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License along with *
+ * this program. If not, see . *
+ ****************************************************************************************/
+
+package com.ichi2.libanki.sync;
+
+import android.os.Build;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.security.KeyStore;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+import okhttp3.ConnectionSpec;
+import okhttp3.OkHttpClient;
+import okhttp3.TlsVersion;
+import timber.log.Timber;
+
+/**
+ * Enables TLS v1.2 when creating SSLSockets.
+ *
+ * For some reason, android supports TLS v1.2 from API 16, but enables it by
+ * default only from API 20. Additionally some Samsung API21 phones also need this.
+ *
+ * @link https://developer.android.com/reference/javax/net/ssl/SSLSocket.html
+ * @see SSLSocketFactory
+ */
+public class Tls12SocketFactory extends SSLSocketFactory {
+ private static final String[] TLS_V12_ONLY = {"TLSv1.2"};
+
+ private final SSLSocketFactory delegate;
+
+
+ public static OkHttpClient.Builder enableTls12OnPreLollipop(OkHttpClient.Builder client) {
+ if (Build.VERSION.SDK_INT >= 16 && Build.VERSION.SDK_INT < 22) {
+
+ try {
+ TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
+ TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init((KeyStore) null);
+ TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
+ if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
+ throw new IllegalStateException("Unexpected default trust managers:"
+ + Arrays.toString(trustManagers));
+ }
+ X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
+ SSLContext sc = SSLContext.getInstance("TLSv1.2");
+ sc.init(null, new TrustManager[] {trustManager}, null);
+ Tls12SocketFactory socketFactory = new Tls12SocketFactory(sc.getSocketFactory());
+ client.sslSocketFactory(socketFactory, trustManager);
+
+ ConnectionSpec cs = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .tlsVersions(TlsVersion.TLS_1_2)
+ .build();
+
+ List specs = new ArrayList<>();
+ specs.add(cs);
+ specs.add(ConnectionSpec.COMPATIBLE_TLS);
+ specs.add(ConnectionSpec.CLEARTEXT);
+
+ client.connectionSpecs(specs);
+ } catch (Exception exc) {
+ Timber.e(exc, "Error while setting TLS 1.2");
+ }
+ }
+
+ return client;
+ }
+
+
+ private Tls12SocketFactory(SSLSocketFactory base) {
+ this.delegate = base;
+ }
+
+
+ @Override
+ public String[] getDefaultCipherSuites() {
+ return delegate.getDefaultCipherSuites();
+ }
+
+
+ @Override
+ public String[] getSupportedCipherSuites() {
+ return delegate.getSupportedCipherSuites();
+ }
+
+
+ @Override
+ public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
+ return patch(delegate.createSocket(s, host, port, autoClose));
+ }
+
+
+ @Override
+ public Socket createSocket(String host, int port) throws IOException {
+ return patch(delegate.createSocket(host, port));
+ }
+
+
+ @Override
+ public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
+ return patch(delegate.createSocket(host, port, localHost, localPort));
+ }
+
+
+ @Override
+ public Socket createSocket(InetAddress host, int port) throws IOException {
+ return patch(delegate.createSocket(host, port));
+ }
+
+
+ @Override
+ public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
+ return patch(delegate.createSocket(address, port, localAddress, localPort));
+ }
+
+
+ private Socket patch(Socket s) {
+ if (s instanceof SSLSocket) {
+ ((SSLSocket) s).setEnabledProtocols(TLS_V12_ONLY);
+ }
+
+ // Note if progress tracking needs to be more granular than default OkHTTP buffer, do this:
+// try {
+// s.setSendBufferSize(16 * 1024);
+// // We will only know if this is a problem if people complain about progress bar going to 100%
+// // on small transfers (indicating buffer ate all contents) before transfer finishes (because buffer is still flushing)
+// // Important to say that this can slow things down dramatically though so needs tuning. With 16kb throughput was 40kb/s
+// // By default throughput was maxing my 50Mbit line out (!)
+// } catch (SocketException se) {
+// Timber.e(se, "Unable to set socket send buffer size");
+// }
+
+ return s;
+ }
+}
\ No newline at end of file
diff --git a/AnkiDroid/src/main/java/org/apache/commons/httpclient/contrib/ssl/EasySSLSocketFactory.java b/AnkiDroid/src/main/java/org/apache/commons/httpclient/contrib/ssl/EasySSLSocketFactory.java
deleted file mode 100644
index c2e2c3530487..000000000000
--- a/AnkiDroid/src/main/java/org/apache/commons/httpclient/contrib/ssl/EasySSLSocketFactory.java
+++ /dev/null
@@ -1,108 +0,0 @@
-
-package org.apache.commons.httpclient.contrib.ssl;
-
-import java.io.IOException;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.Socket;
-
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSocket;
-import javax.net.ssl.TrustManager;
-
-@SuppressWarnings("deprecation") // tracking HTTP transport change in github already
-public class EasySSLSocketFactory implements org.apache.http.conn.scheme.SocketFactory, org.apache.http.conn.scheme.LayeredSocketFactory {
- private SSLContext sslcontext = null;
-
-
- private static SSLContext createEasySSLContext() throws IOException {
- try {
- SSLContext context = SSLContext.getInstance("TLS");
- context.init(null, new TrustManager[] { new EasyX509TrustManager(null) }, null);
- return context;
- } catch (Exception e) {
- throw new IOException(e.getMessage());
- }
- }
-
-
- private SSLContext getSSLContext() throws IOException {
- if (sslcontext == null) {
- sslcontext = createEasySSLContext();
- }
- return sslcontext;
- }
-
-
- /**
- * @see org.apache.http.conn.scheme.SocketFactory#connectSocket(java.net.Socket, java.lang.String, int,
- * java.net.InetAddress, int, org.apache.http.params.HttpParams)
- */
- @Override
- public Socket connectSocket(Socket sock, String host, int port, InetAddress localAddress, int localPort,
- org.apache.http.params.HttpParams params) throws IOException {
- int connTimeout = org.apache.http.params.HttpConnectionParams.getConnectionTimeout(params);
- int soTimeout = org.apache.http.params.HttpConnectionParams.getSoTimeout(params);
- InetSocketAddress remoteAddress = new InetSocketAddress(host, port);
- SSLSocket sslsock = (SSLSocket) ((sock != null) ? sock : createSocket());
-
- if ((localAddress != null) || (localPort > 0)) {
- // we need to bind explicitly
- if (localPort < 0) {
- localPort = 0; // indicates "any"
- }
- InetSocketAddress isa = new InetSocketAddress(localAddress, localPort);
- sslsock.bind(isa);
- }
-
- sslsock.connect(remoteAddress, connTimeout);
- sslsock.setSoTimeout(soTimeout);
- return sslsock;
- }
-
-
- /**
- * @see org.apache.http.conn.scheme.SocketFactory#createSocket()
- */
- @Override
- public Socket createSocket() throws IOException {
- return getSSLContext().getSocketFactory().createSocket();
- }
-
-
- /**
- * @see org.apache.http.conn.scheme.SocketFactory#isSecure(java.net.Socket)
- */
- @Override
- public boolean isSecure(Socket socket) throws IllegalArgumentException {
- return true;
- }
-
-
- /**
- * @see org.apache.http.conn.scheme.LayeredSocketFactory#createSocket(java.net.Socket, java.lang.String, int,
- * boolean)
- */
- @Override
- public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
- return getSSLContext().getSocketFactory().createSocket(socket, host, port, autoClose);
- }
-
-
- // -------------------------------------------------------------------
- // javadoc in org.apache.http.conn.scheme.SocketFactory says :
- // Both Object.equals() and Object.hashCode() must be overridden
- // for the correct operation of some connection managers
- // -------------------------------------------------------------------
-
- @Override
- public boolean equals(Object obj) {
- return ((obj != null) && obj.getClass().equals(EasySSLSocketFactory.class));
- }
-
-
- @Override
- public int hashCode() {
- return EasySSLSocketFactory.class.hashCode();
- }
-}
diff --git a/AnkiDroid/src/main/java/org/apache/commons/httpclient/contrib/ssl/EasyX509TrustManager.java b/AnkiDroid/src/main/java/org/apache/commons/httpclient/contrib/ssl/EasyX509TrustManager.java
deleted file mode 100644
index 7fca1e95c654..000000000000
--- a/AnkiDroid/src/main/java/org/apache/commons/httpclient/contrib/ssl/EasyX509TrustManager.java
+++ /dev/null
@@ -1,102 +0,0 @@
-
-package org.apache.commons.httpclient.contrib.ssl;
-
-import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.cert.CertificateException;
-import java.security.cert.X509Certificate;
-import java.util.Date;
-
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.TrustManagerFactory;
-import javax.net.ssl.X509TrustManager;
-
-public class EasyX509TrustManager implements X509TrustManager {
- private X509TrustManager standardTrustManager = null;
-
-
- /**
- * Constructor for EasyX509TrustManager.
- */
- public EasyX509TrustManager(KeyStore keystore) throws NoSuchAlgorithmException, KeyStoreException {
- super();
- TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
- factory.init(keystore);
- TrustManager[] trustmanagers = factory.getTrustManagers();
- if (trustmanagers.length == 0) {
- throw new NoSuchAlgorithmException("no trust manager found");
- }
- standardTrustManager = (X509TrustManager) trustmanagers[0];
- }
-
-
- /**
- * @see javax.net.ssl.X509TrustManager#checkClientTrusted(X509Certificate[],String authType)
- */
- @Override
- public void checkClientTrusted(X509Certificate[] certificates, String authType) throws CertificateException {
- standardTrustManager.checkClientTrusted(certificates, authType);
- }
-
-
- /**
- * @see javax.net.ssl.X509TrustManager#checkServerTrusted(X509Certificate[],String authType)
- */
- @Override
- public void checkServerTrusted(X509Certificate[] certificates, String authType) throws CertificateException {
- // Clean up the certificates chain and build a new one.
- // Theoretically, we shouldn't have to do this, but various web servers
- // in practice are mis-configured to have out-of-order certificates or
- // expired self-issued root certificate.
- int chainLength = certificates.length;
- if (certificates.length > 1) {
- // 1. we clean the received certificates chain.
- // We start from the end-entity certificate, tracing down by matching
- // the "issuer" field and "subject" field until we can't continue.
- // This helps when the certificates are out of order or
- // some certificates are not related to the site.
- int currIndex;
- for (currIndex = 0; currIndex < certificates.length; ++currIndex) {
- boolean foundNext = false;
- for (int nextIndex = currIndex + 1; nextIndex < certificates.length; ++nextIndex) {
- if (certificates[currIndex].getIssuerDN().equals(certificates[nextIndex].getSubjectDN())) {
- foundNext = true;
- // Exchange certificates so that 0 through currIndex + 1 are in proper order
- if (nextIndex != currIndex + 1) {
- X509Certificate tempCertificate = certificates[nextIndex];
- certificates[nextIndex] = certificates[currIndex + 1];
- certificates[currIndex + 1] = tempCertificate;
- }
- break;
- }
- }
- if (!foundNext) {
- break;
- }
- }
-
- // 2. we exam if the last traced certificate is self issued and it is expired.
- // If so, we drop it and pass the rest to checkServerTrusted(), hoping we might
- // have a similar but unexpired trusted root.
- chainLength = currIndex + 1;
- X509Certificate lastCertificate = certificates[chainLength - 1];
- Date now = new Date();
- if (lastCertificate.getSubjectDN().equals(lastCertificate.getIssuerDN())
- && now.after(lastCertificate.getNotAfter())) {
- --chainLength;
- }
- }
-
- standardTrustManager.checkServerTrusted(certificates, authType);
- }
-
-
- /**
- * @see javax.net.ssl.X509TrustManager#getAcceptedIssuers()
- */
- @Override
- public X509Certificate[] getAcceptedIssuers() {
- return standardTrustManager.getAcceptedIssuers();
- }
-}