0df7f6a4c557e6b65eaed78df79539caef4570cb
[pub/Android/ownCloud.git] / src / com / owncloud / android / network / AdvancedSslSocketFactory.java
1 /* ownCloud Android client application
2 * Copyright (C) 2012 Bartek Przybylski
3 *
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.
8 *
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.
13 *
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/>.
16 *
17 */
18
19 package com.owncloud.android.network;
20
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;
30
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;
39
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;
44
45 import android.util.Log;
46
47 /**
48 * AdvancedSSLProtocolSocketFactory allows to create SSL {@link Socket}s with
49 * a custom SSLContext and an optional Hostname Verifier.
50 *
51 * @author David A. Velasco
52 */
53
54 public class AdvancedSslSocketFactory implements ProtocolSocketFactory {
55
56 private static final String TAG = AdvancedSslSocketFactory.class.getSimpleName();
57
58 private SSLContext mSslContext = null;
59 private AdvancedX509TrustManager mTrustManager = null;
60 private X509HostnameVerifier mHostnameVerifier = null;
61
62 public SSLContext getSslContext() {
63 return mSslContext;
64 }
65
66 /**
67 * Constructor for AdvancedSSLProtocolSocketFactory.
68 */
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;
77 }
78
79 /**
80 * @see ProtocolSocketFactory#createSocket(java.lang.String,int,java.net.InetAddress,int)
81 */
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);
85 return socket;
86 }
87
88
89 /**
90 * Attempts to get a new socket connection to the given host within the
91 * given time limit.
92 *
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}
98 *
99 * @return Socket a new socket
100 *
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
103 * determined
104 */
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");
112 }
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);
123 return socket;
124 }
125
126 /**
127 * @see ProtocolSocketFactory#createSocket(java.lang.String,int)
128 */
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);
134 return socket;
135 }
136
137 public boolean equals(Object obj) {
138 return ((obj != null) && obj.getClass().equals(
139 AdvancedSslSocketFactory.class));
140 }
141
142 public int hashCode() {
143 return AdvancedSslSocketFactory.class.hashCode();
144 }
145
146
147 public X509HostnameVerifier getHostNameVerifier() {
148 return mHostnameVerifier;
149 }
150
151
152 public void setHostNameVerifier(X509HostnameVerifier hostnameVerifier) {
153 mHostnameVerifier = hostnameVerifier;
154 }
155
156 /**
157 * Verifies the identity of the server.
158 *
159 * The server certificate is verified first.
160 *
161 * Then, the host name is compared with the content of the server certificate using the current host name verifier, if any.
162 * @param socket
163 */
164 private void verifyPeerIdentity(String host, int port, Socket socket) throws IOException {
165 try {
166 IOException failInHandshake = null;
167 /// 1. VERIFY THE SERVER CERTIFICATE through the registered TrustManager (that should be an instance of AdvancedX509TrustManager)
168 try {
169 SSLSocket sock = (SSLSocket) socket; // a new SSLSession instance is created as a "side effect"
170 sock.startHandshake();
171 } catch (IOException e) {
172 failInHandshake = e;
173 if (!(e.getCause() instanceof CertificateCombinedException)) {
174 throw e;
175 }
176 }
177
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();
185 try {
186 mHostnameVerifier.verify(host, serverCert);
187 } catch (SSLException e) {
188 verifiedHostname = false;
189 }
190
191 } else {
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);
198
199 } else {
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;
204 ///
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;
217 }
218 }
219 }
220 if (newSession != null) {
221 if (!mTrustManager.isKnownServer((X509Certificate)(newSession.getPeerCertificates()[0]))) {
222 verifiedHostname = mHostnameVerifier.verify(host, newSession);
223 }
224 } else {
225 Log.d(TAG, "Hostname verification could not be performed because the new SSLSession was not found");
226 }
227 }
228 }
229 }
230 }
231
232 /// 3. Combine the exceptions to throw, if any
233 if (failInHandshake != null) {
234 if (!verifiedHostname) {
235 ((CertificateCombinedException)failInHandshake.getCause()).setSslPeerUnverifiedException(new SSLPeerUnverifiedException(host));
236 }
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);
242 pue.initCause(ce);
243 throw pue;
244 }
245
246 } catch (IOException io) {
247 try {
248 socket.close();
249 } catch (Exception x) {
250 // NOTHING - irrelevant exception for the caller
251 }
252 throw io;
253 }
254 }
255
256 }