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:
parent
02e3a90df3
commit
9fe7c995dd
@ -50,7 +50,6 @@ import org.apache.http.conn.ssl.X509HostnameVerifier;
|
||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* AdvancedSSLProtocolSocketFactory allows to create SSL {@link Socket}s with
|
||||
* a custom SSLContext and an optional Hostname Verifier.
|
||||
@ -74,27 +73,27 @@ public class AdvancedSslSocketFactory implements SecureProtocolSocketFactory {
|
||||
* Constructor for AdvancedSSLProtocolSocketFactory.
|
||||
*/
|
||||
public AdvancedSslSocketFactory(
|
||||
SSLContext sslContext, AdvancedX509TrustManager trustManager, X509HostnameVerifier hostnameVerifier
|
||||
) {
|
||||
SSLContext sslContext, AdvancedX509TrustManager trustManager, X509HostnameVerifier hostnameVerifier
|
||||
) {
|
||||
|
||||
if (sslContext == null)
|
||||
throw new IllegalArgumentException("AdvancedSslSocketFactory can not be created with a null SSLContext");
|
||||
if (trustManager == null && mHostnameVerifier != null)
|
||||
throw new IllegalArgumentException(
|
||||
"AdvancedSslSocketFactory can not be created with a null Trust Manager and a " +
|
||||
"not null Hostname Verifier"
|
||||
);
|
||||
"AdvancedSslSocketFactory can not be created with a null Trust Manager and a " +
|
||||
"not null Hostname Verifier"
|
||||
);
|
||||
mSslContext = sslContext;
|
||||
mTrustManager = trustManager;
|
||||
mHostnameVerifier = hostnameVerifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see ProtocolSocketFactory#createSocket(java.lang.String,int,java.net.InetAddress,int)
|
||||
* @see ProtocolSocketFactory#createSocket(java.lang.String, int, java.net.InetAddress, int)
|
||||
*/
|
||||
@Override
|
||||
public Socket createSocket(String host, int port, InetAddress clientHost, int clientPort)
|
||||
throws IOException, UnknownHostException {
|
||||
throws IOException, UnknownHostException {
|
||||
|
||||
Socket socket = mSslContext.getSocketFactory().createSocket(host, port, clientHost, clientPort);
|
||||
enableSecureProtocols(socket);
|
||||
@ -142,27 +141,25 @@ public class AdvancedSslSocketFactory implements SecureProtocolSocketFactory {
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
/**
|
||||
* Attempts to get a new socket connection to the given host within the
|
||||
* given time limit.
|
||||
*
|
||||
* @param host the host name/IP
|
||||
* @param port the port on the host
|
||||
* @param clientHost the local host name/IP to bind the socket to
|
||||
* @param clientPort the port on the local machine
|
||||
* @param params {@link HttpConnectionParams Http connection parameters}
|
||||
*
|
||||
* @param host the host name/IP
|
||||
* @param port the port on the host
|
||||
* @param localAddress the local host name/IP to bind the socket to
|
||||
* @param localPort the port on the local machine
|
||||
* @param params {@link HttpConnectionParams Http connection parameters}
|
||||
* @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
|
||||
* determined
|
||||
* determined
|
||||
*/
|
||||
@Override
|
||||
public Socket createSocket(final String host, final int port,
|
||||
final InetAddress localAddress, final int localPort,
|
||||
final HttpConnectionParams params) throws IOException,
|
||||
UnknownHostException, ConnectTimeoutException {
|
||||
final InetAddress localAddress, final int localPort,
|
||||
final HttpConnectionParams params) throws IOException,
|
||||
UnknownHostException, ConnectTimeoutException {
|
||||
Log_OC.d(TAG, "Creating SSL Socket with remote " + host + ":" + port + ", local " + localAddress + ":" +
|
||||
localPort + ", params: " + params);
|
||||
if (params == null) {
|
||||
@ -179,20 +176,21 @@ public class AdvancedSslSocketFactory implements SecureProtocolSocketFactory {
|
||||
SocketAddress localaddr = new InetSocketAddress(localAddress, localPort);
|
||||
SocketAddress remoteaddr = new InetSocketAddress(host, port);
|
||||
socket.setSoTimeout(params.getSoTimeout());
|
||||
WriteTimeoutEnforcer.setSoWriteTimeout(params.getSoTimeout(), socket);
|
||||
socket.bind(localaddr);
|
||||
ServerNameIndicator.setServerNameIndication(host, (SSLSocket)socket);
|
||||
ServerNameIndicator.setServerNameIndication(host, (SSLSocket) socket);
|
||||
socket.connect(remoteaddr, timeout);
|
||||
verifyPeerIdentity(host, port, socket);
|
||||
return socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see ProtocolSocketFactory#createSocket(java.lang.String,int)
|
||||
/**
|
||||
* @see ProtocolSocketFactory#createSocket(java.lang.String, int)
|
||||
*/
|
||||
@Override
|
||||
public Socket createSocket(String host, int port) throws IOException,
|
||||
UnknownHostException {
|
||||
Log_OC.d(TAG, "Creating SSL Socket with remote " + host + ":" + port);
|
||||
UnknownHostException {
|
||||
Log_OC.d(TAG, "Creating SSL Socket with remote " + host + ":" + port);
|
||||
Socket socket = mSslContext.getSocketFactory().createSocket(host, port);
|
||||
enableSecureProtocols(socket);
|
||||
verifyPeerIdentity(host, port, socket);
|
||||
@ -200,19 +198,19 @@ public class AdvancedSslSocketFactory implements SecureProtocolSocketFactory {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Override
|
||||
public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException,
|
||||
UnknownHostException {
|
||||
Socket sslSocket = mSslContext.getSocketFactory().createSocket(socket, host, port, autoClose);
|
||||
enableSecureProtocols(sslSocket);
|
||||
verifyPeerIdentity(host, port, sslSocket);
|
||||
return sslSocket;
|
||||
}
|
||||
UnknownHostException {
|
||||
Socket sslSocket = mSslContext.getSocketFactory().createSocket(socket, host, port, autoClose);
|
||||
enableSecureProtocols(sslSocket);
|
||||
verifyPeerIdentity(host, port, sslSocket);
|
||||
return sslSocket;
|
||||
}
|
||||
|
||||
|
||||
public boolean equals(Object obj) {
|
||||
return ((obj != null) && obj.getClass().equals(
|
||||
AdvancedSslSocketFactory.class));
|
||||
AdvancedSslSocketFactory.class));
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
@ -231,11 +229,12 @@ public class AdvancedSslSocketFactory implements SecureProtocolSocketFactory {
|
||||
|
||||
/**
|
||||
* Verifies the identity of the server.
|
||||
*
|
||||
* <p>
|
||||
* 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,
|
||||
* if any.
|
||||
* if any.
|
||||
*
|
||||
* @param socket
|
||||
*/
|
||||
private void verifyPeerIdentity(String host, int port, Socket socket) throws IOException {
|
||||
@ -254,14 +253,14 @@ public class AdvancedSslSocketFactory implements SecureProtocolSocketFactory {
|
||||
} else {
|
||||
Throwable cause = e.getCause();
|
||||
Throwable previousCause = null;
|
||||
while ( cause != null &&
|
||||
cause != previousCause &&
|
||||
!(cause instanceof CertificateCombinedException)) {
|
||||
while (cause != null &&
|
||||
cause != previousCause &&
|
||||
!(cause instanceof CertificateCombinedException)) {
|
||||
previousCause = cause;
|
||||
cause = cause.getCause();
|
||||
}
|
||||
if (cause != null && cause instanceof CertificateCombinedException) {
|
||||
failInHandshake = (CertificateCombinedException)cause;
|
||||
failInHandshake = (CertificateCombinedException) cause;
|
||||
}
|
||||
}
|
||||
if (failInHandshake == null) {
|
||||
@ -286,8 +285,8 @@ public class AdvancedSslSocketFactory implements SecureProtocolSocketFactory {
|
||||
|
||||
} else {
|
||||
/// 2.2 : a new SSLSession instance was created in the handshake
|
||||
newSession = ((SSLSocket)socket).getSession();
|
||||
if (!mTrustManager.isKnownServer((X509Certificate)(newSession.getPeerCertificates()[0]))) {
|
||||
newSession = ((SSLSocket) socket).getSession();
|
||||
if (!mTrustManager.isKnownServer((X509Certificate) (newSession.getPeerCertificates()[0]))) {
|
||||
verifiedHostname = mHostnameVerifier.verify(host, newSession);
|
||||
}
|
||||
}
|
||||
@ -296,12 +295,12 @@ public class AdvancedSslSocketFactory implements SecureProtocolSocketFactory {
|
||||
/// 3. Combine the exceptions to throw, if any
|
||||
if (!verifiedHostname) {
|
||||
SSLPeerUnverifiedException pue = new SSLPeerUnverifiedException(
|
||||
"Names in the server certificate do not match to " + host + " in the URL"
|
||||
);
|
||||
"Names in the server certificate do not match to " + host + " in the URL"
|
||||
);
|
||||
if (failInHandshake == null) {
|
||||
failInHandshake = new CertificateCombinedException(
|
||||
(X509Certificate) newSession.getPeerCertificates()[0]
|
||||
);
|
||||
(X509Certificate) newSession.getPeerCertificates()[0]
|
||||
);
|
||||
failInHandshake.setHostInUrl(host);
|
||||
}
|
||||
failInHandshake.setSslPeerUnverifiedException(pue);
|
||||
@ -324,22 +323,22 @@ public class AdvancedSslSocketFactory implements SecureProtocolSocketFactory {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants that all protocols supported by the Security Provider in mSslContext are enabled in socket.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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
|
||||
* http://developer.android.com/reference/javax/net/ssl/SSLSocket.html.
|
||||
*
|
||||
* @param 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
|
||||
* 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
|
||||
* 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.
|
||||
*
|
||||
* @param socket
|
||||
*/
|
||||
private void enableSecureProtocols(Socket socket) {
|
||||
SSLParameters params = mSslContext.getSupportedSSLParameters();
|
||||
String [] supportedProtocols = params.getProtocols();
|
||||
((SSLSocket) socket).setEnabledProtocols(supportedProtocols);
|
||||
SSLParameters params = mSslContext.getSupportedSSLParameters();
|
||||
String[] supportedProtocols = params.getProtocols();
|
||||
((SSLSocket) socket).setEnabledProtocols(supportedProtocols);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user