1
0
mirror of https://github.com/owncloud/android-library.git synced 2025-06-08 00:16:09 +00:00

Add write timeout if socket implementation supports it, so that chances of blocking and upload if network is removed are minimized

This commit is contained in:
David A. Velasco 2017-04-21 16:10:16 +02:00
parent 02e3a90df3
commit 9fe7c995dd
2 changed files with 264 additions and 92 deletions

View File

@ -50,7 +50,6 @@ import org.apache.http.conn.ssl.X509HostnameVerifier;
import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.common.utils.Log_OC;
/** /**
* AdvancedSSLProtocolSocketFactory allows to create SSL {@link Socket}s with * AdvancedSSLProtocolSocketFactory allows to create SSL {@link Socket}s with
* a custom SSLContext and an optional Hostname Verifier. * a custom SSLContext and an optional Hostname Verifier.
@ -90,7 +89,7 @@ public class AdvancedSslSocketFactory implements SecureProtocolSocketFactory {
} }
/** /**
* @see ProtocolSocketFactory#createSocket(java.lang.String,int,java.net.InetAddress,int) * @see ProtocolSocketFactory#createSocket(java.lang.String, int, java.net.InetAddress, int)
*/ */
@Override @Override
public Socket createSocket(String host, int port, InetAddress clientHost, int clientPort) public Socket createSocket(String host, int port, InetAddress clientHost, int clientPort)
@ -148,12 +147,10 @@ public class AdvancedSslSocketFactory implements SecureProtocolSocketFactory {
* *
* @param host the host name/IP * @param host the host name/IP
* @param port the port on the host * @param port the port on the host
* @param clientHost the local host name/IP to bind the socket to * @param localAddress the local host name/IP to bind the socket to
* @param clientPort the port on the local machine * @param localPort the port on the local machine
* @param params {@link HttpConnectionParams Http connection parameters} * @param params {@link HttpConnectionParams Http connection parameters}
*
* @return Socket a new socket * @return Socket a new socket
*
* @throws IOException if an I/O error occurs while creating the socket * @throws IOException if an I/O error occurs while creating the socket
* @throws UnknownHostException if the IP address of the host cannot be * @throws UnknownHostException if the IP address of the host cannot be
* determined * determined
@ -179,15 +176,16 @@ public class AdvancedSslSocketFactory implements SecureProtocolSocketFactory {
SocketAddress localaddr = new InetSocketAddress(localAddress, localPort); SocketAddress localaddr = new InetSocketAddress(localAddress, localPort);
SocketAddress remoteaddr = new InetSocketAddress(host, port); SocketAddress remoteaddr = new InetSocketAddress(host, port);
socket.setSoTimeout(params.getSoTimeout()); socket.setSoTimeout(params.getSoTimeout());
WriteTimeoutEnforcer.setSoWriteTimeout(params.getSoTimeout(), socket);
socket.bind(localaddr); socket.bind(localaddr);
ServerNameIndicator.setServerNameIndication(host, (SSLSocket)socket); ServerNameIndicator.setServerNameIndication(host, (SSLSocket) socket);
socket.connect(remoteaddr, timeout); socket.connect(remoteaddr, timeout);
verifyPeerIdentity(host, port, socket); verifyPeerIdentity(host, port, socket);
return socket; return socket;
} }
/** /**
* @see ProtocolSocketFactory#createSocket(java.lang.String,int) * @see ProtocolSocketFactory#createSocket(java.lang.String, int)
*/ */
@Override @Override
public Socket createSocket(String host, int port) throws IOException, public Socket createSocket(String host, int port) throws IOException,
@ -231,11 +229,12 @@ public class AdvancedSslSocketFactory implements SecureProtocolSocketFactory {
/** /**
* Verifies the identity of the server. * Verifies the identity of the server.
* * <p>
* The server certificate is verified first. * The server certificate is verified first.
* * <p>
* Then, the host name is compared with the content of the server certificate using the current host name verifier, * Then, the host name is compared with the content of the server certificate using the current host name verifier,
* if any. * if any.
*
* @param socket * @param socket
*/ */
private void verifyPeerIdentity(String host, int port, Socket socket) throws IOException { private void verifyPeerIdentity(String host, int port, Socket socket) throws IOException {
@ -254,14 +253,14 @@ public class AdvancedSslSocketFactory implements SecureProtocolSocketFactory {
} else { } else {
Throwable cause = e.getCause(); Throwable cause = e.getCause();
Throwable previousCause = null; Throwable previousCause = null;
while ( cause != null && while (cause != null &&
cause != previousCause && cause != previousCause &&
!(cause instanceof CertificateCombinedException)) { !(cause instanceof CertificateCombinedException)) {
previousCause = cause; previousCause = cause;
cause = cause.getCause(); cause = cause.getCause();
} }
if (cause != null && cause instanceof CertificateCombinedException) { if (cause != null && cause instanceof CertificateCombinedException) {
failInHandshake = (CertificateCombinedException)cause; failInHandshake = (CertificateCombinedException) cause;
} }
} }
if (failInHandshake == null) { if (failInHandshake == null) {
@ -286,8 +285,8 @@ public class AdvancedSslSocketFactory implements SecureProtocolSocketFactory {
} else { } else {
/// 2.2 : a new SSLSession instance was created in the handshake /// 2.2 : a new SSLSession instance was created in the handshake
newSession = ((SSLSocket)socket).getSession(); newSession = ((SSLSocket) socket).getSession();
if (!mTrustManager.isKnownServer((X509Certificate)(newSession.getPeerCertificates()[0]))) { if (!mTrustManager.isKnownServer((X509Certificate) (newSession.getPeerCertificates()[0]))) {
verifiedHostname = mHostnameVerifier.verify(host, newSession); verifiedHostname = mHostnameVerifier.verify(host, newSession);
} }
} }
@ -326,10 +325,10 @@ public class AdvancedSslSocketFactory implements SecureProtocolSocketFactory {
/** /**
* Grants that all protocols supported by the Security Provider in mSslContext are enabled in socket. * Grants that all protocols supported by the Security Provider in mSslContext are enabled in socket.
* * <p>
* Grants also that no unsupported protocol is tried to be enabled. That would trigger an exception, breaking * Grants also that no unsupported protocol is tried to be enabled. That would trigger an exception, breaking
* the connection process although some protocols are supported. * the connection process although some protocols are supported.
* * <p>
* This is not cosmetic: not all the supported protocols are enabled by default. Too see an overview of * This is not cosmetic: not all the supported protocols are enabled by default. Too see an overview of
* supported and enabled protocols in the stock Security Provider in Android see the tables in * supported and enabled protocols in the stock Security Provider in Android see the tables in
* http://developer.android.com/reference/javax/net/ssl/SSLSocket.html. * http://developer.android.com/reference/javax/net/ssl/SSLSocket.html.
@ -338,7 +337,7 @@ public class AdvancedSslSocketFactory implements SecureProtocolSocketFactory {
*/ */
private void enableSecureProtocols(Socket socket) { private void enableSecureProtocols(Socket socket) {
SSLParameters params = mSslContext.getSupportedSSLParameters(); SSLParameters params = mSslContext.getSupportedSSLParameters();
String [] supportedProtocols = params.getProtocols(); String[] supportedProtocols = params.getProtocols();
((SSLSocket) socket).setEnabledProtocols(supportedProtocols); ((SSLSocket) socket).setEnabledProtocols(supportedProtocols);
} }

View File

@ -0,0 +1,173 @@
/* ownCloud Android Library is available under MIT license
* Copyright (C) 2017 ownCloud GmbH.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
package com.owncloud.android.lib.common.network;
import com.owncloud.android.lib.common.utils.Log_OC;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.Socket;
import java.util.concurrent.atomic.AtomicReference;
/**
* Enforces, if possible, a write timeout for a socket.
*
* Built as a singleton.
*
* Tries to hit something like this:
* https://android.googlesource.com/platform/external/conscrypt/+/lollipop-release/src/main/java/org/conscrypt/OpenSSLSocketImpl.java#1005
*
* Minimizes the chances of getting stalled in PUT/POST request if the network interface is lost while
* writing the entity into the outwards sockect.
*
* It happens. See https://github.com/owncloud/android/issues/1684#issuecomment-295306015
*
* @author David A. Velasco
*/
public class WriteTimeoutEnforcer {
private static final String TAG = WriteTimeoutEnforcer.class.getSimpleName();
private static final AtomicReference<WriteTimeoutEnforcer> mSingleInstance = new AtomicReference<>();
private static final String METHOD_NAME = "setSoWriteTimeout";
private final WeakReference<Class<?>> mSocketClassRef;
private final WeakReference<Method> mSetSoWriteTimeoutMethodRef;
/**
* Private constructor, class is a singleton.
*
* @param socketClass Underlying implementation class of {@link Socket} used to connect
* with the server.
* @param setSoWriteTimeoutMethod Name of the method to call to set a write timeout in the socket.
*/
private WriteTimeoutEnforcer(Class<?> socketClass, Method setSoWriteTimeoutMethod) {
mSocketClassRef = new WeakReference<Class<?>>(socketClass);
mSetSoWriteTimeoutMethodRef =
(setSoWriteTimeoutMethod == null) ?
null :
new WeakReference<>(setSoWriteTimeoutMethod)
;
}
/**
* Calls the {@code #setSoWrite(int)} method of the underlying implementation
* of {@link Socket} if exists.
* Creates and initializes the single instance of the class when needed
*
* @param writeTimeoutMilliseconds Write timeout to set, in milliseconds.
* @param socket Client socket to connect with the server.
*/
public static void setSoWriteTimeout(int writeTimeoutMilliseconds, Socket socket) {
final Method setSoWriteTimeoutMethod = getMethod(socket);
if (setSoWriteTimeoutMethod != null) {
try {
setSoWriteTimeoutMethod.invoke(socket, writeTimeoutMilliseconds);
Log_OC.i(
TAG,
"Write timeout set in socket, writeTimeoutMilliseconds: "
+ writeTimeoutMilliseconds
);
} catch (IllegalArgumentException e) {
Log_OC.e(TAG, "Call to (SocketImpl)#setSoWriteTimeout(int) failed ", e);
} catch (IllegalAccessException e) {
Log_OC.e(TAG, "Call to (SocketImpl)#setSoWriteTimeout(int) failed ", e);
} catch (InvocationTargetException e) {
Log_OC.e(TAG, "Call to (SocketImpl)#setSoWriteTimeout(int) failed ", e);
}
} else {
Log_OC.i(TAG, "Write timeout for socket not supported");
}
}
/**
* Gets the method to invoke trying to minimize the cost of reflection reusing objects cached
* in static members.
*
* @param socket Instance of the socket to use in connection with server.
* @return Method to call to set a write timeout in the socket.
*/
private static Method getMethod(Socket socket) {
final Class<?> socketClass = socket.getClass();
final WriteTimeoutEnforcer instance = mSingleInstance.get();
if (instance == null) {
return initFrom(socketClass);
} else if (instance.mSocketClassRef.get() != socketClass) {
// the underlying class changed
return initFrom(socketClass);
} else if (instance.mSetSoWriteTimeoutMethodRef == null) {
// method not supported
return null;
} else {
final Method cachedSetSoWriteTimeoutMethod = instance.mSetSoWriteTimeoutMethodRef.get();
return (cachedSetSoWriteTimeoutMethod == null) ?
initFrom(socketClass) :
cachedSetSoWriteTimeoutMethod
;
}
}
/**
* Singleton initializer.
*
* Uses reflection to extract and 'cache' the method to invoke to set a write timouet in a socket.
*
* @param socketClass Underlying class providing the implementation of {@link Socket}.
* @return Method to call to set a write timeout in the socket.
*/
private static Method initFrom(Class<?> socketClass) {
Log_OC.i(TAG, "Socket implementation: " + socketClass.getCanonicalName());
Method setSoWriteTimeoutMethod = null;
try {
setSoWriteTimeoutMethod = socketClass.getMethod(METHOD_NAME, int.class);
} catch (SecurityException e) {
Log_OC.e(TAG, "Could not access to (SocketImpl)#setSoWriteTimeout(int) method ", e);
} catch (NoSuchMethodException e) {
Log_OC.i(
TAG,
"Could not find (SocketImpl)#setSoWriteTimeout(int) method - write timeout not supported"
);
}
mSingleInstance.set(new WriteTimeoutEnforcer(socketClass, setSoWriteTimeoutMethod));
return setSoWriteTimeoutMethod;
}
}