Skip to content

Commit eddcf3e

Browse files
committed
Merge pull request android-async-http#606 from fineswap/CustomCASample
New example to demo use of self-signed CA's in the library.
2 parents 6bc22c4 + 7f4d3fa commit eddcf3e

File tree

6 files changed

+415
-2
lines changed

6 files changed

+415
-2
lines changed
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
Android Asynchronous Http Client Sample
3+
Copyright (c) 2014 Marek Sebera <[email protected]>
4+
http://loopj.com
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
*/
18+
19+
package com.loopj.android.http.sample;
20+
21+
import android.os.Bundle;
22+
import android.util.Log;
23+
import android.widget.Toast;
24+
25+
import java.io.IOException;
26+
import java.io.InputStream;
27+
28+
import org.apache.http.Header;
29+
import org.apache.http.HttpEntity;
30+
31+
32+
import com.loopj.android.http.AsyncHttpClient;
33+
import com.loopj.android.http.BinaryHttpResponseHandler;
34+
import com.loopj.android.http.RequestHandle;
35+
import com.loopj.android.http.ResponseHandlerInterface;
36+
import com.loopj.android.http.sample.util.SecureSocketFactory;
37+
38+
import java.security.KeyManagementException;
39+
import java.security.KeyStore;
40+
import java.security.KeyStoreException;
41+
import java.security.NoSuchAlgorithmException;
42+
import java.security.UnrecoverableKeyException;
43+
import java.security.cert.CertificateException;
44+
45+
/**
46+
* This sample demonstrates the implementation of self-signed CA's and connection
47+
* to servers with such certificates. Be sure to read 'res/raw/custom_ca.txt'
48+
* for how-to instructions on how to generate a BKS file necessary for this
49+
* sample.
50+
*
51+
* @author Noor Dawod <[email protected]>
52+
*/
53+
public class CustomCASample extends SampleParentActivity {
54+
55+
private static final String LOG_TAG = "CustomCASample";
56+
57+
private static final String SERVER_TEST_URL = "https://httpbin.org/get";
58+
private static final String STORE_ALIAS = "TheAlias";
59+
private static final String STORE_PASS = "ThePass";
60+
61+
// Instruct the library to retry connection when this exception is raised.
62+
static {
63+
AsyncHttpClient.allowRetryExceptionClass(javax.net.ssl.SSLException.class);
64+
}
65+
66+
@Override
67+
protected void onCreate(Bundle savedInstanceState) {
68+
super.onCreate(savedInstanceState);
69+
try {
70+
InputStream is = null;
71+
try {
72+
// Configure the library to use a custom 'bks' file to perform
73+
// SSL negotiation.
74+
KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType());
75+
is = getResources().openRawResource(R.raw.store);
76+
store.load(is, STORE_PASS.toCharArray());
77+
getAsyncHttpClient().setSSLSocketFactory(new SecureSocketFactory(store, STORE_ALIAS));
78+
} catch(IOException e) {
79+
throw new KeyStoreException(e);
80+
} catch(CertificateException e) {
81+
throw new KeyStoreException(e);
82+
} catch(NoSuchAlgorithmException e) {
83+
throw new KeyStoreException(e);
84+
} catch(KeyManagementException e) {
85+
throw new KeyStoreException(e);
86+
} catch(UnrecoverableKeyException e) {
87+
throw new KeyStoreException(e);
88+
} finally {
89+
AsyncHttpClient.silentCloseInputStream(is);
90+
}
91+
} catch(KeyStoreException e) {
92+
Log.e(LOG_TAG, "Unable to initialize key store", e);
93+
Toast.makeText(
94+
this,
95+
"Please read res/raw/custom_ca.txt\nto learn how to create your own\nkey store containing a custom CA",
96+
Toast.LENGTH_LONG).show();
97+
}
98+
}
99+
100+
@Override
101+
public int getSampleTitle() {
102+
return R.string.title_custom_ca;
103+
}
104+
105+
@Override
106+
public boolean isRequestBodyAllowed() {
107+
return true;
108+
}
109+
110+
@Override
111+
public boolean isRequestHeadersAllowed() {
112+
return false;
113+
}
114+
115+
@Override
116+
public String getDefaultURL() {
117+
return SERVER_TEST_URL;
118+
}
119+
120+
@Override
121+
public ResponseHandlerInterface getResponseHandler() {
122+
return new BinaryHttpResponseHandler() {
123+
@Override
124+
public void onStart() {
125+
clearOutputs();
126+
}
127+
128+
@Override
129+
public String[] getAllowedContentTypes() {
130+
// Allowing all data for debug purposes
131+
return new String[]{".*"};
132+
}
133+
134+
public void onSuccess(int statusCode, Header[] headers, byte[] binaryData) {
135+
debugStatusCode(LOG_TAG, statusCode);
136+
debugHeaders(LOG_TAG, headers);
137+
debugResponse(LOG_TAG, "Received response is " + binaryData.length + " bytes");
138+
}
139+
140+
@Override
141+
public void onFailure(int statusCode, Header[] headers, byte[] errorResponse, Throwable e) {
142+
debugHeaders(LOG_TAG, headers);
143+
debugStatusCode(LOG_TAG, statusCode);
144+
debugThrowable(LOG_TAG, e);
145+
if (errorResponse != null) {
146+
debugResponse(LOG_TAG, "Received response is " + errorResponse.length + " bytes");
147+
}
148+
}
149+
};
150+
}
151+
152+
@Override
153+
public RequestHandle executeSample(AsyncHttpClient client, String URL, Header[] headers, HttpEntity entity, ResponseHandlerInterface responseHandler) {
154+
return client.get(this, URL, headers, null, responseHandler);
155+
}
156+
}

sample/src/main/java/com/loopj/android/http/sample/WaypointsActivity.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ public class WaypointsActivity extends ListActivity {
4747
new SampleConfig(R.string.title_synchronous, SynchronousClientSample.class),
4848
new SampleConfig(R.string.title_intent_service_sample, IntentServiceSample.class),
4949
new SampleConfig(R.string.title_post_files, FilesSample.class),
50-
new SampleConfig(R.string.title_persistent_cookies, PersistentCookiesSample.class)
50+
new SampleConfig(R.string.title_persistent_cookies, PersistentCookiesSample.class),
51+
new SampleConfig(R.string.title_custom_ca, CustomCASample.class)
5152
};
5253

5354
@Override
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/*
2+
Android Asynchronous Http Client Sample
3+
Copyright (c) 2014 Marek Sebera <[email protected]>
4+
http://loopj.com
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
*/
18+
19+
package com.loopj.android.http.sample.util;
20+
21+
import android.util.Log;
22+
23+
import com.loopj.android.http.AsyncHttpClient;
24+
import org.apache.http.conn.ssl.SSLSocketFactory;
25+
26+
import java.lang.reflect.Field;
27+
import java.io.ByteArrayInputStream;
28+
import java.io.IOException;
29+
import java.io.InputStream;
30+
import java.net.InetAddress;
31+
import java.net.Socket;
32+
import java.net.UnknownHostException;
33+
import java.security.cert.Certificate;
34+
import java.security.cert.X509Certificate;
35+
import java.security.InvalidKeyException;
36+
import java.security.KeyManagementException;
37+
import java.security.KeyStore;
38+
import java.security.KeyStoreException;
39+
import java.security.NoSuchAlgorithmException;
40+
import java.security.NoSuchProviderException;
41+
import java.security.SignatureException;
42+
import java.security.UnrecoverableKeyException;
43+
import java.security.cert.CertificateException;
44+
import java.security.cert.CertificateFactory;
45+
46+
import javax.net.ssl.SSLContext;
47+
import javax.net.ssl.TrustManager;
48+
import javax.net.ssl.X509TrustManager;
49+
50+
/**
51+
* A class to authenticate a secured connection against a custom CA using a
52+
* BKS store.
53+
*
54+
* @author Noor Dawod <[email protected]>
55+
*/
56+
public class SecureSocketFactory extends SSLSocketFactory {
57+
58+
private static final String LOG_TAG = "SecureSocketFactory";
59+
60+
private final SSLContext sslCtx;
61+
private final X509Certificate[] acceptedIssuers;
62+
63+
/**
64+
* Instantiate a new secured factory pertaining to the passed store. Be sure
65+
* to initialize the store with the password using
66+
* {@link java.security.KeyStore#load(java.io.InputStream, char[])} method.
67+
*
68+
* @param store The key store holding the certificate details
69+
* @param alias The alias of the certificate to use
70+
* @throws CertificateException
71+
* @throws NoSuchAlgorithmException
72+
* @throws KeyManagementException
73+
* @throws KeyStoreException
74+
* @throws UnrecoverableKeyException
75+
*/
76+
public SecureSocketFactory(KeyStore store, String alias)
77+
throws
78+
CertificateException,
79+
NoSuchAlgorithmException,
80+
KeyManagementException,
81+
KeyStoreException,
82+
UnrecoverableKeyException {
83+
84+
super(store);
85+
86+
// Loading the CA certificate from store.
87+
final Certificate rootca = store.getCertificate(alias);
88+
89+
// Turn it to X509 format.
90+
InputStream is = new ByteArrayInputStream(rootca.getEncoded());
91+
X509Certificate x509ca = (X509Certificate)CertificateFactory.getInstance("X.509").generateCertificate(is);
92+
AsyncHttpClient.silentCloseInputStream(is);
93+
94+
if(null == x509ca) {
95+
throw new CertificateException("Embedded SSL certificate has expired.");
96+
}
97+
98+
// Check the CA's validity.
99+
x509ca.checkValidity();
100+
101+
// Accepted CA is only the one installed in the store.
102+
acceptedIssuers = new X509Certificate[]{x509ca};
103+
104+
sslCtx = SSLContext.getInstance("TLS");
105+
sslCtx.init(
106+
null,
107+
new TrustManager[] {
108+
new X509TrustManager() {
109+
@Override
110+
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
111+
}
112+
113+
@Override
114+
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
115+
Exception error = null;
116+
117+
if(null == chain || 0 == chain.length) {
118+
error = new CertificateException("Certificate chain is invalid.");
119+
} else if(null == authType || 0 == authType.length()) {
120+
error = new CertificateException("Authentication type is invalid.");
121+
} else {
122+
Log.i(LOG_TAG, "Chain includes " + chain.length + " certificates.");
123+
try {
124+
for(X509Certificate cert : chain) {
125+
Log.i(LOG_TAG, "Server Certificate Details:");
126+
Log.i(LOG_TAG, "---------------------------");
127+
Log.i(LOG_TAG, "IssuerDN: " + cert.getIssuerDN().toString());
128+
Log.i(LOG_TAG, "SubjectDN: " + cert.getSubjectDN().toString());
129+
Log.i(LOG_TAG, "Serial Number: " + cert.getSerialNumber());
130+
Log.i(LOG_TAG, "Version: " + cert.getVersion());
131+
Log.i(LOG_TAG, "Not before: " + cert.getNotBefore().toString());
132+
Log.i(LOG_TAG, "Not after: " + cert.getNotAfter().toString());
133+
Log.i(LOG_TAG, "---------------------------");
134+
135+
// Make sure that it hasn't expired.
136+
cert.checkValidity();
137+
138+
// Verify the certificate's public key chain.
139+
cert.verify(rootca.getPublicKey());
140+
}
141+
} catch(InvalidKeyException e) {
142+
error = e;
143+
} catch(NoSuchAlgorithmException e) {
144+
error = e;
145+
} catch(NoSuchProviderException e) {
146+
error = e;
147+
} catch(SignatureException e) {
148+
error = e;
149+
}
150+
}
151+
if(null != error) {
152+
Log.e(LOG_TAG, "Certificate error", error);
153+
throw new CertificateException(error);
154+
}
155+
}
156+
157+
@Override
158+
public X509Certificate[] getAcceptedIssuers() {
159+
return acceptedIssuers;
160+
}
161+
}
162+
},
163+
null
164+
);
165+
166+
setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
167+
}
168+
169+
@Override
170+
public Socket createSocket(Socket socket, String host, int port, boolean autoClose)
171+
throws IOException, UnknownHostException {
172+
173+
injectHostname(socket, host);
174+
return sslCtx.getSocketFactory().createSocket(socket, host, port, autoClose);
175+
}
176+
177+
@Override
178+
public Socket createSocket() throws IOException {
179+
return sslCtx.getSocketFactory().createSocket();
180+
}
181+
182+
/**
183+
* Pre-ICS Android had a bug resolving HTTPS addresses. This workaround
184+
* fixes that bug.
185+
*
186+
* @param socket The socket to alter
187+
* @param host Hostname to connect to
188+
* @see https://code.google.com/p/android/issues/detail?id=13117#c14
189+
*/
190+
private void injectHostname(Socket socket, String host) {
191+
if(android.os.Build.VERSION.SDK_INT < 14) {
192+
try {
193+
Field field = InetAddress.class.getDeclaredField("hostName");
194+
field.setAccessible(true);
195+
field.set(socket.getInetAddress(), host);
196+
} catch(Exception ignored) {
197+
}
198+
}
199+
}
200+
}

0 commit comments

Comments
 (0)