Skip to content
This repository was archived by the owner on Mar 11, 2022. It is now read-only.

Commit 4e3b35e

Browse files
committed
Enabled TLSv1.2 support on older Android versions
Added Android version and TLS support checks as well as a TLSv1.2 only SSLSocketFactory to enable on older versions. Moved default interceptors into CouchClient from ReplicatorBuilder. Updated HttpTest that called HttpConnection directly to consume default interceptors. Refactored HttpTest slightly to simplify interceptor addition. Updated replication tests for new assertions, due to interceptor move to CouchClient. Fixed up replication test base for new interceptor assertion sizes. Added CHANGES entry.
1 parent 96a00f9 commit 4e3b35e

File tree

5 files changed

+192
-76
lines changed

5 files changed

+192
-76
lines changed

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Unreleased
2+
- [IMPROVED] Forced a TLS1.2 `SSLSocketFactory` where possible on Android API versions < 20 (it is
3+
already enabled by default on newer API levels).
4+
15
# 2.2.0 (2018-02-14)
26
- [NEW] Added API for specifying a mango selector in the filtered pull replicator
37
- [IMPROVED] Improved efficiency of sub-query when picking winning

cloudant-sync-datastore-core/src/main/java/com/cloudant/sync/internal/mazha/CouchClient.java

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import com.cloudant.http.HttpConnection;
2626
import com.cloudant.http.HttpConnectionRequestInterceptor;
2727
import com.cloudant.http.HttpConnectionResponseInterceptor;
28+
import com.cloudant.http.internal.interceptors.SSLCustomizerInterceptor;
29+
import com.cloudant.http.internal.interceptors.UserAgentInterceptor;
2830
import com.cloudant.sync.internal.common.RetriableTask;
2931
import com.cloudant.sync.internal.documentstore.DocumentRevsList;
3032
import com.cloudant.sync.internal.documentstore.MultipartAttachmentWriter;
@@ -39,11 +41,16 @@
3941
import java.io.InputStream;
4042
import java.io.InputStreamReader;
4143
import java.lang.reflect.Type;
44+
import java.net.InetAddress;
45+
import java.net.Socket;
4246
import java.net.URI;
4347
import java.nio.charset.Charset;
48+
import java.security.KeyManagementException;
49+
import java.security.NoSuchAlgorithmException;
4450
import java.util.ArrayList;
4551
import java.util.Arrays;
4652
import java.util.Collection;
53+
import java.util.Collections;
4754
import java.util.HashMap;
4855
import java.util.List;
4956
import java.util.Map;
@@ -52,8 +59,25 @@
5259
import java.util.logging.Level;
5360
import java.util.logging.Logger;
5461

62+
import javax.net.ssl.SSLContext;
63+
import javax.net.ssl.SSLSocket;
64+
import javax.net.ssl.SSLSocketFactory;
65+
5566
public class CouchClient {
5667

68+
public static final List<HttpConnectionRequestInterceptor> DEFAULT_REQUEST_INTERCEPTORS;
69+
70+
// Set up the defaults
71+
static {
72+
HttpConnectionRequestInterceptor ua_interceptor =
73+
new UserAgentInterceptor(CouchClient.class.getClassLoader(),
74+
"META-INF/com.cloudant.sync.client.properties");
75+
HttpConnectionRequestInterceptor tlsInterceptor = checkAndGetTlsInterceptor();
76+
DEFAULT_REQUEST_INTERCEPTORS = (tlsInterceptor != null) ?
77+
Arrays.asList(ua_interceptor, tlsInterceptor) :
78+
Collections.singletonList(ua_interceptor);
79+
}
80+
5781
private CouchURIHelper uriHelper;
5882
private List<HttpConnectionRequestInterceptor> requestInterceptors;
5983
private List<HttpConnectionResponseInterceptor> responseInterceptors;
@@ -66,6 +90,8 @@ public CouchClient(URI rootUri,
6690
this.requestInterceptors = new ArrayList<HttpConnectionRequestInterceptor>();
6791
this.responseInterceptors = new ArrayList<HttpConnectionResponseInterceptor>();
6892

93+
this.requestInterceptors.addAll(DEFAULT_REQUEST_INTERCEPTORS);
94+
6995
if (requestInterceptors != null) {
7096
this.requestInterceptors.addAll(requestInterceptors);
7197
}
@@ -75,6 +101,121 @@ public CouchClient(URI rootUri,
75101
}
76102
}
77103

104+
private static SSLCustomizerInterceptor checkAndGetTlsInterceptor() {
105+
// Some assistance for TLSv1.2 support. Two things we check before we try to force TLSv1.2
106+
// so that we don't interfere with any other custom configuration that may have been
107+
// provided:
108+
// i) are we running on Android, and an old version of Android (api level < 20 does not
109+
// have TLSv1.2 enabled by default)
110+
// ii) does the default SSLContext have TLSv1.2 enabled or available
111+
if (Misc.isRunningOnAndroid()) {
112+
// This block catches all exceptions from TLSv1.2 checks, if we get exceptions we bail
113+
// with a RuntimeException
114+
try {
115+
// Get the API level reflectively so we don't need to import classes only available
116+
// in Android
117+
int androidApiLevel = Class.forName("android.os.Build$VERSION").getField
118+
("SDK_INT").getInt(null);
119+
// If we are on an old version of Android and TLSv1.2 has not been enabled already
120+
// on the default context then we add a special interceptor
121+
if (androidApiLevel < 20) {
122+
// Check i has passed we are on an old version of Android
123+
124+
// Get the default SSLContext
125+
SSLContext defSslCtx = SSLContext.getDefault();
126+
127+
// Check the default protocols for TLSv1.2
128+
if (!Arrays.asList(defSslCtx.getDefaultSSLParameters().getProtocols())
129+
.contains(TlsOnlySslSocketFactory.TLSv12) &&
130+
Arrays.asList(defSslCtx.getSupportedSSLParameters().getProtocols())
131+
.contains(TlsOnlySslSocketFactory.TLSv12)) {
132+
// Check ii has passed TLSv1.2 is not already enabled on the default context
133+
// but is supported on the default context so we can use it
134+
135+
// In an ideal world we would also check the default SSLSocketFactory that
136+
// is set on the HttpsUrlConnection to see if that has been customized from
137+
// the default, but there isn't really a way to check this, equals is not
138+
// overridden on the SSLSocketFactory and there is no route back to the
139+
// SSLContext that generated the SSLSocketFactory.
140+
SSLContext sc = SSLContext.getInstance(TlsOnlySslSocketFactory.TLSv12);
141+
sc.init(null, null, null);
142+
// We add this interceptor, but it could be overridden by a later user
143+
// provided interceptor, so this shouldn't change anyone's existing
144+
// customizations.
145+
SSLSocketFactory s = sc.getSocketFactory();
146+
return new SSLCustomizerInterceptor(new TlsOnlySslSocketFactory(s));
147+
}
148+
}
149+
} catch (NoSuchAlgorithmException e) {
150+
throw new RuntimeException(e);
151+
} catch (NoSuchFieldException e) {
152+
throw new RuntimeException(e);
153+
} catch (IllegalAccessException e) {
154+
throw new RuntimeException(e);
155+
} catch (ClassNotFoundException e) {
156+
throw new RuntimeException(e);
157+
} catch (KeyManagementException e) {
158+
throw new RuntimeException(e);
159+
}
160+
}
161+
return null;
162+
}
163+
164+
private static final class TlsOnlySslSocketFactory extends SSLSocketFactory {
165+
166+
private static final String TLSv12 = "TLSv1.2";
167+
private static final String[] TLSv12Only = new String[]{TLSv12};
168+
private final SSLSocketFactory delegate;
169+
170+
private TlsOnlySslSocketFactory(SSLSocketFactory delegate) {
171+
this.delegate = delegate;
172+
}
173+
174+
@Override
175+
public String[] getDefaultCipherSuites() {
176+
return delegate.getDefaultCipherSuites();
177+
}
178+
179+
@Override
180+
public String[] getSupportedCipherSuites() {
181+
return delegate.getSupportedCipherSuites();
182+
}
183+
184+
@Override
185+
public Socket createSocket(Socket socket, String s, int i, boolean b) throws IOException {
186+
return tlsOnlySocket(delegate.createSocket(socket, s, i, b));
187+
}
188+
189+
@Override
190+
public Socket createSocket(String s, int i) throws IOException {
191+
return tlsOnlySocket(delegate.createSocket(s, i));
192+
}
193+
194+
@Override
195+
public Socket createSocket(String s, int i, InetAddress inetAddress, int i1) throws
196+
IOException {
197+
return tlsOnlySocket(delegate.createSocket(s, i, inetAddress, i1));
198+
}
199+
200+
@Override
201+
public Socket createSocket(InetAddress inetAddress, int i) throws IOException {
202+
return tlsOnlySocket(delegate.createSocket(inetAddress, i));
203+
}
204+
205+
@Override
206+
public Socket createSocket(InetAddress inetAddress, int i, InetAddress inetAddress1, int
207+
i1) throws IOException {
208+
return tlsOnlySocket(delegate.createSocket(inetAddress, i, inetAddress1, i1));
209+
}
210+
211+
private Socket tlsOnlySocket(Socket s) {
212+
if (s instanceof SSLSocket) {
213+
((SSLSocket) s).setEnabledProtocols(TLSv12Only);
214+
}
215+
return s;
216+
}
217+
}
218+
78219
public URI getRootUri() {
79220
return this.uriHelper.getRootUri();
80221
}

cloudant-sync-datastore-core/src/main/java/com/cloudant/sync/replication/ReplicatorBuilder.java

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import com.cloudant.http.HttpConnectionResponseInterceptor;
1919
import com.cloudant.http.internal.interceptors.CookieInterceptor;
2020
import com.cloudant.http.internal.interceptors.IamCookieInterceptor;
21-
import com.cloudant.http.internal.interceptors.UserAgentInterceptor;
2221
import com.cloudant.sync.documentstore.DocumentStore;
2322
import com.cloudant.sync.internal.replication.PullStrategy;
2423
import com.cloudant.sync.internal.replication.PushStrategy;
@@ -40,10 +39,6 @@
4039
// S = Source Type, T = target Type, E = Extending class Type
4140
public abstract class ReplicatorBuilder<S, T, E> {
4241

43-
private static final UserAgentInterceptor USER_AGENT_INTERCEPTOR =
44-
new UserAgentInterceptor(ReplicatorBuilder.class.getClassLoader(),
45-
"META-INF/com.cloudant.sync.client.properties");
46-
4742
private T target;
4843

4944
private S source;
@@ -62,10 +57,6 @@ public abstract class ReplicatorBuilder<S, T, E> {
6257

6358
private String iamApiKey = null;
6459

65-
private ReplicatorBuilder() {
66-
requestInterceptors.add(USER_AGENT_INTERCEPTOR);
67-
}
68-
6960
private int getDefaultPort(URI uri) {
7061

7162
String uriProtocol = uri.getScheme();

cloudant-sync-datastore-core/src/test/java/com/cloudant/http/HttpTest.java

Lines changed: 35 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.cloudant.sync.internal.util.JSONUtils;
2424

2525
import org.junit.Assert;
26+
import org.junit.Before;
2627
import org.junit.Test;
2728
import org.junit.experimental.categories.Category;
2829

@@ -40,6 +41,32 @@
4041
public class HttpTest extends CouchTestBase {
4142

4243
private String data = "{\"hello\":\"world\"}";
44+
private ByteArrayInputStream bis;
45+
46+
@Before
47+
public void setupDataByteStream() {
48+
this.bis = new ByteArrayInputStream(data.getBytes());
49+
}
50+
51+
private HttpConnection postAndAssertNothingReadBeforeSettingBodyGenerator(CouchConfig config)
52+
throws Exception {
53+
HttpConnection conn = new HttpConnection("POST", config.getRootUri().toURL(),
54+
"application/json");
55+
56+
// nothing read from stream
57+
Assert.assertEquals(bis.available(), data.getBytes().length);
58+
59+
conn.setRequestBody(new HttpConnection.InputStreamGenerator() {
60+
@Override
61+
public InputStream getInputStream() {
62+
return bis;
63+
}
64+
});
65+
// This test invokes HttpConnection directly rather than via the CouchClient, so we need to
66+
// force the addition of some default interceptors
67+
conn.requestInterceptors.addAll(CouchClient.DEFAULT_REQUEST_INTERCEPTORS);
68+
return conn;
69+
}
4370

4471
/*
4572
* Test "Expect: 100-Continue" header works as expected
@@ -54,21 +81,9 @@ public class HttpTest extends CouchTestBase {
5481
* whilst we are still writing).
5582
*/
5683
@Test
57-
public void testExpect100Continue() throws IOException {
84+
public void testExpect100Continue() throws Exception {
5885
CouchConfig config = getCouchConfig("no_such_database");
59-
HttpConnection conn = new HttpConnection("POST", config.getRootUri().toURL(),
60-
"application/json");
61-
final ByteArrayInputStream bis = new ByteArrayInputStream(data.getBytes());
62-
63-
// nothing read from stream
64-
Assert.assertEquals(bis.available(), data.getBytes().length);
65-
66-
conn.setRequestBody(new HttpConnection.InputStreamGenerator() {
67-
@Override
68-
public InputStream getInputStream() {
69-
return bis;
70-
}
71-
});
86+
HttpConnection conn = postAndAssertNothingReadBeforeSettingBodyGenerator(config);
7287
boolean thrown = false;
7388
try {
7489
conn.execute();
@@ -87,24 +102,12 @@ public InputStream getInputStream() {
87102
* Basic test that we can write a document body by POSTing to a known database
88103
*/
89104
@Test
90-
public void testWriteToServerOk() throws IOException {
105+
public void testWriteToServerOk() throws Exception {
91106
CouchConfig config = getCouchConfig("httptest" + System.currentTimeMillis());
92107
CouchClient client = new CouchClient(config.getRootUri(), config.getRequestInterceptors()
93108
, config.getResponseInterceptors());
94109
client.createDb();
95-
HttpConnection conn = new HttpConnection("POST", config.getRootUri().toURL(),
96-
"application/json");
97-
final ByteArrayInputStream bis = new ByteArrayInputStream(data.getBytes());
98-
99-
// nothing read from stream
100-
Assert.assertEquals(bis.available(), data.getBytes().length);
101-
102-
conn.setRequestBody(new HttpConnection.InputStreamGenerator() {
103-
@Override
104-
public InputStream getInputStream() {
105-
return bis;
106-
}
107-
});
110+
HttpConnection conn = postAndAssertNothingReadBeforeSettingBodyGenerator(config);
108111
conn.execute();
109112

110113
// stream was read to end
@@ -117,24 +120,12 @@ public InputStream getInputStream() {
117120
* without first calling execute()
118121
*/
119122
@Test
120-
public void testReadBeforeExecute() throws IOException {
123+
public void testReadBeforeExecute() throws Exception {
121124
CouchConfig config = getCouchConfig("httptest" + System.currentTimeMillis());
122125
CouchClient client = new CouchClient(config.getRootUri(), config.getRequestInterceptors()
123126
, config.getResponseInterceptors());
124127
client.createDb();
125-
HttpConnection conn = new HttpConnection("POST", config.getRootUri().toURL(),
126-
"application/json");
127-
final ByteArrayInputStream bis = new ByteArrayInputStream(data.getBytes());
128-
129-
// nothing read from stream
130-
Assert.assertEquals(bis.available(), data.getBytes().length);
131-
132-
conn.setRequestBody(new HttpConnection.InputStreamGenerator() {
133-
@Override
134-
public InputStream getInputStream() {
135-
return bis;
136-
}
137-
});
128+
HttpConnection conn = postAndAssertNothingReadBeforeSettingBodyGenerator(config);
138129
try {
139130
conn.responseAsString();
140131
Assert.fail("IOException not thrown as expected");
@@ -156,7 +147,7 @@ public InputStream getInputStream() {
156147
// be named cookie_test
157148
//
158149
@Test
159-
public void testCookieAuthWithoutRetry() throws IOException {
150+
public void testCookieAuthWithoutRetry() throws Exception {
160151

161152
if (TestOptions.IGNORE_AUTH_HEADERS) {
162153
return;
@@ -168,21 +159,9 @@ public void testCookieAuthWithoutRetry() throws IOException {
168159
.currentTimeMillis()).getRootUri().toString());
169160

170161
CouchConfig config = getCouchConfig("cookie_test");
171-
HttpConnection conn = new HttpConnection("POST", config.getRootUri().toURL(),
172-
"application/json");
162+
HttpConnection conn = postAndAssertNothingReadBeforeSettingBodyGenerator(config);
173163
conn.responseInterceptors.add(interceptor);
174164
conn.requestInterceptors.add(interceptor);
175-
final ByteArrayInputStream bis = new ByteArrayInputStream(data.getBytes());
176-
177-
// nothing read from stream
178-
Assert.assertEquals(bis.available(), data.getBytes().length);
179-
180-
conn.setRequestBody(new HttpConnection.InputStreamGenerator() {
181-
@Override
182-
public InputStream getInputStream() {
183-
return bis;
184-
}
185-
});
186165
conn.execute();
187166

188167
// stream was read to end

0 commit comments

Comments
 (0)