1 /* ownCloud Android client application
2 * Copyright (C) 2012 Bartek Przybylski
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 package com
.owncloud
.android
.network
;
21 import java
.io
.IOException
;
22 import java
.net
.InetAddress
;
23 import java
.net
.InetSocketAddress
;
24 import java
.net
.Socket
;
25 import java
.net
.SocketAddress
;
26 import java
.net
.UnknownHostException
;
27 import java
.security
.cert
.Certificate
;
28 import java
.security
.cert
.X509Certificate
;
29 import java
.util
.Enumeration
;
31 import javax
.net
.SocketFactory
;
32 import javax
.net
.ssl
.SSLContext
;
33 import javax
.net
.ssl
.SSLException
;
34 import javax
.net
.ssl
.SSLPeerUnverifiedException
;
35 import javax
.net
.ssl
.SSLSession
;
36 import javax
.net
.ssl
.SSLSessionContext
;
37 import javax
.net
.ssl
.SSLSocket
;
38 import javax
.net
.ssl
.X509TrustManager
;
40 import org
.apache
.commons
.httpclient
.ConnectTimeoutException
;
41 import org
.apache
.commons
.httpclient
.params
.HttpConnectionParams
;
42 import org
.apache
.commons
.httpclient
.protocol
.ProtocolSocketFactory
;
43 import org
.apache
.http
.conn
.ssl
.X509HostnameVerifier
;
45 import android
.util
.Log
;
48 * AdvancedSSLProtocolSocketFactory allows to create SSL {@link Socket}s with
49 * a custom SSLContext and an optional Hostname Verifier.
51 * @author David A. Velasco
54 public class AdvancedSslSocketFactory
implements ProtocolSocketFactory
{
56 private static final String TAG
= AdvancedSslSocketFactory
.class.getSimpleName();
58 private SSLContext mSslContext
= null
;
59 private AdvancedX509TrustManager mTrustManager
= null
;
60 private X509HostnameVerifier mHostnameVerifier
= null
;
62 public SSLContext
getSslContext() {
67 * Constructor for AdvancedSSLProtocolSocketFactory.
69 public AdvancedSslSocketFactory(SSLContext sslContext
, AdvancedX509TrustManager trustManager
, X509HostnameVerifier hostnameVerifier
) {
70 if (sslContext
== null
)
71 throw new IllegalArgumentException("AdvancedSslSocketFactory can not be created with a null SSLContext");
72 if (trustManager
== null
)
73 throw new IllegalArgumentException("AdvancedSslSocketFactory can not be created with a null Trust Manager");
74 mSslContext
= sslContext
;
75 mTrustManager
= trustManager
;
76 mHostnameVerifier
= hostnameVerifier
;
80 * @see ProtocolSocketFactory#createSocket(java.lang.String,int,java.net.InetAddress,int)
82 public Socket
createSocket(String host
, int port
, InetAddress clientHost
, int clientPort
) throws IOException
, UnknownHostException
{
83 Socket socket
= mSslContext
.getSocketFactory().createSocket(host
, port
, clientHost
, clientPort
);
84 verifyPeerIdentity(host
, port
, socket
);
90 * Attempts to get a new socket connection to the given host within the
93 * @param host the host name/IP
94 * @param port the port on the host
95 * @param clientHost the local host name/IP to bind the socket to
96 * @param clientPort the port on the local machine
97 * @param params {@link HttpConnectionParams Http connection parameters}
99 * @return Socket a new socket
101 * @throws IOException if an I/O error occurs while creating the socket
102 * @throws UnknownHostException if the IP address of the host cannot be
105 public Socket
createSocket(final String host
, final int port
,
106 final InetAddress localAddress
, final int localPort
,
107 final HttpConnectionParams params
) throws IOException
,
108 UnknownHostException
, ConnectTimeoutException
{
109 Log
.d(TAG
, "Creating SSL Socket with remote " + host
+ ":" + port
+ ", local " + localAddress
+ ":" + localPort
+ ", params: " + params
);
110 if (params
== null
) {
111 throw new IllegalArgumentException("Parameters may not be null");
113 int timeout
= params
.getConnectionTimeout();
114 SocketFactory socketfactory
= mSslContext
.getSocketFactory();
115 Log
.d(TAG
, " ... with connection timeout " + timeout
+ " and socket timeout " + params
.getSoTimeout());
116 Socket socket
= socketfactory
.createSocket();
117 SocketAddress localaddr
= new InetSocketAddress(localAddress
, localPort
);
118 SocketAddress remoteaddr
= new InetSocketAddress(host
, port
);
119 socket
.setSoTimeout(params
.getSoTimeout());
120 socket
.bind(localaddr
);
121 socket
.connect(remoteaddr
, timeout
);
122 verifyPeerIdentity(host
, port
, socket
);
127 * @see ProtocolSocketFactory#createSocket(java.lang.String,int)
129 public Socket
createSocket(String host
, int port
) throws IOException
,
130 UnknownHostException
{
131 Log
.d(TAG
, "Creating SSL Socket with remote " + host
+ ":" + port
);
132 Socket socket
= mSslContext
.getSocketFactory().createSocket(host
, port
);
133 verifyPeerIdentity(host
, port
, socket
);
137 public boolean equals(Object obj
) {
138 return ((obj
!= null
) && obj
.getClass().equals(
139 AdvancedSslSocketFactory
.class));
142 public int hashCode() {
143 return AdvancedSslSocketFactory
.class.hashCode();
147 public X509HostnameVerifier
getHostNameVerifier() {
148 return mHostnameVerifier
;
152 public void setHostNameVerifier(X509HostnameVerifier hostnameVerifier
) {
153 mHostnameVerifier
= hostnameVerifier
;
157 * Verifies the identity of the server.
159 * The server certificate is verified first.
161 * Then, the host name is compared with the content of the server certificate using the current host name verifier, if any.
164 private void verifyPeerIdentity(String host
, int port
, Socket socket
) throws IOException
{
166 IOException failInHandshake
= null
;
167 /// 1. VERIFY THE SERVER CERTIFICATE through the registered TrustManager (that should be an instance of AdvancedX509TrustManager)
169 SSLSocket sock
= (SSLSocket
) socket
; // a new SSLSession instance is created as a "side effect"
170 sock
.startHandshake();
171 } catch (IOException e
) {
173 if (!(e
.getCause() instanceof CertificateCombinedException
)) {
178 /// 2. VERIFY HOSTNAME
179 SSLSession newSession
= null
;
180 boolean verifiedHostname
= true
;
181 if (mHostnameVerifier
!= null
) {
182 if (failInHandshake
!= null
) {
183 /// 2.1 : a new SSLSession instance was NOT created in the handshake
184 X509Certificate serverCert
= ((CertificateCombinedException
)failInHandshake
.getCause()).getServerCertificate();
186 mHostnameVerifier
.verify(host
, serverCert
);
187 } catch (SSLException e
) {
188 verifiedHostname
= false
;
192 /// 2.2 : a new SSLSession instance was created in the handshake
193 if (android
.os
.Build
.VERSION
.SDK_INT
>= android
.os
.Build
.VERSION_CODES
.HONEYCOMB
) {
194 /// this is sure ONLY for Android >= 3.0 ; the same is true for mHostnameVerifier.verify(host, (SSLSocket)socket)
195 newSession
= ((SSLSocket
)socket
).getSession();
196 if (!mTrustManager
.isKnownServer((X509Certificate
)(newSession
.getPeerCertificates()[0])))
197 verifiedHostname
= mHostnameVerifier
.verify(host
, newSession
);
200 //// performing the previous verification in Android versions under 2.3.x (and we don't know the exact value of x) WILL BREAK THE SSL CONTEXT, and any HTTP operation executed later through the socket WILL FAIL ;
201 //// it is related with A BUG IN THE OpenSSLSOcketImpl.java IN THE ANDROID CORE SYSTEM; it was fixed here:
202 //// http://gitorious.org/ginger/libcore/blobs/df349b3eaf4d1fa0643ab722173bc3bf20a266f5/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLSocketImpl.java
203 /// but we could not find out in what Android version was released the bug fix;
205 /// besides, due to the bug, calling ((SSLSocket)socket).getSession() IS NOT SAFE ; the next workaround is an UGLY BUT SAFE solution to get it
206 SSLSessionContext sessionContext
= mSslContext
.getClientSessionContext();
207 if (sessionContext
!= null
) {
208 SSLSession session
= null
;
209 synchronized(sessionContext
) { // a SSLSession in the SSLSessionContext can be closed while we are searching for the new one; it happens; really
210 Enumeration
<byte[]> ids
= sessionContext
.getIds();
211 while (ids
.hasMoreElements()) {
212 session
= sessionContext
.getSession(ids
.nextElement());
213 if ( session
.getPeerHost().equals(host
) &&
214 session
.getPeerPort() == port
&&
215 (newSession
== null
|| newSession
.getCreationTime() < session
.getCreationTime())) {
216 newSession
= session
;
220 if (newSession
!= null
) {
221 if (!mTrustManager
.isKnownServer((X509Certificate
)(newSession
.getPeerCertificates()[0]))) {
222 verifiedHostname
= mHostnameVerifier
.verify(host
, newSession
);
225 Log
.d(TAG
, "Hostname verification could not be performed because the new SSLSession was not found");
232 /// 3. Combine the exceptions to throw, if any
233 if (failInHandshake
!= null
) {
234 if (!verifiedHostname
) {
235 ((CertificateCombinedException
)failInHandshake
.getCause()).setSslPeerUnverifiedException(new SSLPeerUnverifiedException(host
));
237 throw failInHandshake
;
238 } else if (!verifiedHostname
) {
239 CertificateCombinedException ce
= new CertificateCombinedException((X509Certificate
) newSession
.getPeerCertificates()[0]);
240 SSLPeerUnverifiedException pue
= new SSLPeerUnverifiedException(host
);
241 ce
.setSslPeerUnverifiedException(pue
);
246 } catch (IOException io
) {
249 } catch (Exception x
) {
250 // NOTHING - irrelevant exception for the caller