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(); - } -}