1 /* 2 * Copyright 2015 The gRPC Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package io.grpc.okhttp; 18 19 import static com.google.common.base.Preconditions.checkNotNull; 20 21 import com.google.common.annotations.VisibleForTesting; 22 import io.grpc.internal.GrpcUtil; 23 import io.grpc.okhttp.internal.OptionalMethod; 24 import io.grpc.okhttp.internal.Platform; 25 import io.grpc.okhttp.internal.Platform.TlsExtensionType; 26 import io.grpc.okhttp.internal.Protocol; 27 import io.grpc.okhttp.internal.Util; 28 import java.io.IOException; 29 import java.lang.reflect.Constructor; 30 import java.lang.reflect.InvocationTargetException; 31 import java.lang.reflect.Method; 32 import java.net.Socket; 33 import java.util.ArrayList; 34 import java.util.Arrays; 35 import java.util.Collections; 36 import java.util.List; 37 import java.util.logging.Level; 38 import java.util.logging.Logger; 39 import javax.annotation.Nullable; 40 import javax.net.ssl.SSLParameters; 41 import javax.net.ssl.SSLSocket; 42 43 /** 44 * A helper class located in package com.squareup.okhttp.internal for TLS negotiation. 45 */ 46 class OkHttpProtocolNegotiator { 47 private static final Logger logger = Logger.getLogger(OkHttpProtocolNegotiator.class.getName()); 48 private static final Platform DEFAULT_PLATFORM = Platform.get(); 49 private static OkHttpProtocolNegotiator NEGOTIATOR = 50 createNegotiator(OkHttpProtocolNegotiator.class.getClassLoader()); 51 52 protected final Platform platform; 53 54 @VisibleForTesting OkHttpProtocolNegotiator(Platform platform)55 OkHttpProtocolNegotiator(Platform platform) { 56 this.platform = checkNotNull(platform, "platform"); 57 } 58 get()59 public static OkHttpProtocolNegotiator get() { 60 return NEGOTIATOR; 61 } 62 63 /** 64 * Creates corresponding negotiator according to whether on Android. 65 */ 66 @VisibleForTesting createNegotiator(ClassLoader loader)67 static OkHttpProtocolNegotiator createNegotiator(ClassLoader loader) { 68 boolean android = true; 69 try { 70 // Attempt to find Android 2.3+ APIs. 71 loader.loadClass("com.android.org.conscrypt.OpenSSLSocketImpl"); 72 } catch (ClassNotFoundException e1) { 73 logger.log(Level.FINE, "Unable to find Conscrypt. Skipping", e1); 74 try { 75 // Older platform before being unbundled. 76 loader.loadClass("org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl"); 77 } catch (ClassNotFoundException e2) { 78 logger.log(Level.FINE, "Unable to find any OpenSSLSocketImpl. Skipping", e2); 79 android = false; 80 } 81 } 82 return android 83 ? new AndroidNegotiator(DEFAULT_PLATFORM) 84 : new OkHttpProtocolNegotiator(DEFAULT_PLATFORM); 85 } 86 87 /** 88 * Start and wait until the negotiation is done, returns the negotiated protocol. 89 * 90 * @throws IOException if an IO error was encountered during the handshake. 91 * @throws RuntimeException if the negotiation completed, but no protocol was selected. 92 */ negotiate( SSLSocket sslSocket, String hostname, @Nullable List<Protocol> protocols)93 public String negotiate( 94 SSLSocket sslSocket, String hostname, @Nullable List<Protocol> protocols) throws IOException { 95 if (protocols != null) { 96 configureTlsExtensions(sslSocket, hostname, protocols); 97 } 98 try { 99 // Force handshake. 100 sslSocket.startHandshake(); 101 102 String negotiatedProtocol = getSelectedProtocol(sslSocket); 103 if (negotiatedProtocol == null) { 104 throw new RuntimeException("TLS ALPN negotiation failed with protocols: " + protocols); 105 } 106 return negotiatedProtocol; 107 } finally { 108 platform.afterHandshake(sslSocket); 109 } 110 } 111 112 /** Configure TLS extensions. */ configureTlsExtensions( SSLSocket sslSocket, String hostname, List<Protocol> protocols)113 protected void configureTlsExtensions( 114 SSLSocket sslSocket, String hostname, List<Protocol> protocols) { 115 platform.configureTlsExtensions(sslSocket, hostname, protocols); 116 } 117 118 /** Returns the negotiated protocol, or null if no protocol was negotiated. */ getSelectedProtocol(SSLSocket socket)119 public String getSelectedProtocol(SSLSocket socket) { 120 return platform.getSelectedProtocol(socket); 121 } 122 123 @VisibleForTesting 124 static final class AndroidNegotiator extends OkHttpProtocolNegotiator { 125 // setUseSessionTickets(boolean) 126 private static final OptionalMethod<Socket> SET_USE_SESSION_TICKETS = 127 new OptionalMethod<>(null, "setUseSessionTickets", Boolean.TYPE); 128 // setHostname(String) 129 private static final OptionalMethod<Socket> SET_HOSTNAME = 130 new OptionalMethod<>(null, "setHostname", String.class); 131 // byte[] getAlpnSelectedProtocol() 132 private static final OptionalMethod<Socket> GET_ALPN_SELECTED_PROTOCOL = 133 new OptionalMethod<>(byte[].class, "getAlpnSelectedProtocol"); 134 // setAlpnProtocol(byte[]) 135 private static final OptionalMethod<Socket> SET_ALPN_PROTOCOLS = 136 new OptionalMethod<>(null, "setAlpnProtocols", byte[].class); 137 // byte[] getNpnSelectedProtocol() 138 private static final OptionalMethod<Socket> GET_NPN_SELECTED_PROTOCOL = 139 new OptionalMethod<>(byte[].class, "getNpnSelectedProtocol"); 140 // setNpnProtocol(byte[]) 141 private static final OptionalMethod<Socket> SET_NPN_PROTOCOLS = 142 new OptionalMethod<>(null, "setNpnProtocols", byte[].class); 143 144 // Non-null on Android 10.0+. 145 // SSLSockets.isSupportedSocket(SSLSocket) 146 private static final Method SSL_SOCKETS_IS_SUPPORTED_SOCKET; 147 // SSLSockets.setUseSessionTickets(SSLSocket, boolean) 148 private static final Method SSL_SOCKETS_SET_USE_SESSION_TICKET; 149 // SSLParameters.setApplicationProtocols(String[]) 150 private static final Method SET_APPLICATION_PROTOCOLS; 151 // SSLParameters.getApplicationProtocols() 152 private static final Method GET_APPLICATION_PROTOCOLS; 153 // SSLSocket.getApplicationProtocol() 154 private static final Method GET_APPLICATION_PROTOCOL; 155 156 // Non-null on Android 7.0+. 157 // SSLParameters.setServerNames(List<SNIServerName>) 158 private static final Method SET_SERVER_NAMES; 159 // SNIHostName(String) 160 private static final Constructor<?> SNI_HOST_NAME; 161 162 static { 163 // Attempt to find Android 10.0+ APIs. 164 Method setApplicationProtocolsMethod = null; 165 Method getApplicationProtocolsMethod = null; 166 Method getApplicationProtocolMethod = null; 167 Method sslSocketsIsSupportedSocketMethod = null; 168 Method sslSocketsSetUseSessionTicketsMethod = null; 169 try { 170 Class<?> sslParameters = SSLParameters.class; 171 setApplicationProtocolsMethod = 172 sslParameters.getMethod("setApplicationProtocols", String[].class); 173 getApplicationProtocolsMethod = sslParameters.getMethod("getApplicationProtocols"); 174 getApplicationProtocolMethod = SSLSocket.class.getMethod("getApplicationProtocol"); 175 Class<?> sslSockets = Class.forName("android.net.ssl.SSLSockets"); 176 sslSocketsIsSupportedSocketMethod = 177 sslSockets.getMethod("isSupportedSocket", SSLSocket.class); 178 sslSocketsSetUseSessionTicketsMethod = 179 sslSockets.getMethod("setUseSessionTickets", SSLSocket.class, boolean.class); 180 } catch (ClassNotFoundException e) { 181 logger.log(Level.FINER, "Failed to find Android 10.0+ APIs", e); 182 } catch (NoSuchMethodException e) { 183 logger.log(Level.FINER, "Failed to find Android 10.0+ APIs", e); 184 } 185 SET_APPLICATION_PROTOCOLS = setApplicationProtocolsMethod; 186 GET_APPLICATION_PROTOCOLS = getApplicationProtocolsMethod; 187 GET_APPLICATION_PROTOCOL = getApplicationProtocolMethod; 188 SSL_SOCKETS_IS_SUPPORTED_SOCKET = sslSocketsIsSupportedSocketMethod; 189 SSL_SOCKETS_SET_USE_SESSION_TICKET = sslSocketsSetUseSessionTicketsMethod; 190 191 // Attempt to find Android 7.0+ APIs. 192 Method setServerNamesMethod = null; 193 Constructor<?> sniHostNameConstructor = null; 194 try { 195 setServerNamesMethod = SSLParameters.class.getMethod("setServerNames", List.class); 196 sniHostNameConstructor = 197 Class.forName("javax.net.ssl.SNIHostName").getConstructor(String.class); 198 } catch (ClassNotFoundException e) { 199 logger.log(Level.FINER, "Failed to find Android 7.0+ APIs", e); 200 } catch (NoSuchMethodException e) { 201 logger.log(Level.FINER, "Failed to find Android 7.0+ APIs", e); 202 } 203 SET_SERVER_NAMES = setServerNamesMethod; 204 SNI_HOST_NAME = sniHostNameConstructor; 205 } 206 AndroidNegotiator(Platform platform)207 AndroidNegotiator(Platform platform) { 208 super(platform); 209 } 210 211 @Override negotiate(SSLSocket sslSocket, String hostname, List<Protocol> protocols)212 public String negotiate(SSLSocket sslSocket, String hostname, List<Protocol> protocols) 213 throws IOException { 214 // First check if a protocol has already been selected, since it's possible that the user 215 // provided SSLSocketFactory has already done the handshake when creates the SSLSocket. 216 String negotiatedProtocol = getSelectedProtocol(sslSocket); 217 if (negotiatedProtocol == null) { 218 negotiatedProtocol = super.negotiate(sslSocket, hostname, protocols); 219 } 220 return negotiatedProtocol; 221 } 222 223 /** 224 * Override {@link Platform}'s configureTlsExtensions for Android older than 5.0, since OkHttp 225 * (2.3+) only support such function for Android 5.0+. 226 * 227 * <p>Note: Prior to Android Q, the standard way of accessing some Conscrypt features was to 228 * use reflection to call hidden APIs. Beginning in Q, there is public API for all of these 229 * features. We attempt to use the public API where possible. Otherwise, fall back to use the 230 * old reflective API. 231 */ 232 @Override configureTlsExtensions( SSLSocket sslSocket, String hostname, List<Protocol> protocols)233 protected void configureTlsExtensions( 234 SSLSocket sslSocket, String hostname, List<Protocol> protocols) { 235 String[] protocolNames = protocolIds(protocols); 236 SSLParameters sslParams = sslSocket.getSSLParameters(); 237 try { 238 // Enable SNI and session tickets. 239 // Hostname is normally validated in the builder (see checkAuthority) and it should 240 // virtually always succeed. Check again here to avoid troubles (e.g., hostname with 241 // underscore) enabling SNI, which works around cases where checkAuthority is disabled. 242 // See b/154375837. 243 if (hostname != null && isValidHostName(hostname)) { 244 if (SSL_SOCKETS_IS_SUPPORTED_SOCKET != null 245 && (boolean) SSL_SOCKETS_IS_SUPPORTED_SOCKET.invoke(null, sslSocket)) { 246 SSL_SOCKETS_SET_USE_SESSION_TICKET.invoke(null, sslSocket, true); 247 } else { 248 SET_USE_SESSION_TICKETS.invokeOptionalWithoutCheckedException(sslSocket, true); 249 } 250 if (SET_SERVER_NAMES != null && SNI_HOST_NAME != null) { 251 SET_SERVER_NAMES 252 .invoke(sslParams, Collections.singletonList(SNI_HOST_NAME.newInstance(hostname))); 253 } else { 254 SET_HOSTNAME.invokeOptionalWithoutCheckedException(sslSocket, hostname); 255 } 256 } 257 boolean alpnEnabled = false; 258 if (GET_APPLICATION_PROTOCOL != null) { 259 try { 260 // If calling SSLSocket.getApplicationProtocol() throws UnsupportedOperationException, 261 // the underlying provider does not implement operations for enabling 262 // ALPN in the fashion of SSLParameters.setApplicationProtocols(). Fall back to 263 // use old hidden methods. 264 GET_APPLICATION_PROTOCOL.invoke(sslSocket); 265 SET_APPLICATION_PROTOCOLS.invoke(sslParams, (Object) protocolNames); 266 alpnEnabled = true; 267 } catch (InvocationTargetException e) { 268 Throwable targetException = e.getTargetException(); 269 if (targetException instanceof UnsupportedOperationException) { 270 logger.log(Level.FINER, "setApplicationProtocol unsupported, will try old methods"); 271 } else { 272 throw e; 273 } 274 } 275 } 276 sslSocket.setSSLParameters(sslParams); 277 // Check application protocols are configured correctly. If not, configure again with 278 // old methods. 279 // Workaround for Conscrypt bug: https://github.com/google/conscrypt/issues/832 280 if (alpnEnabled && GET_APPLICATION_PROTOCOLS != null) { 281 String[] configuredProtocols = 282 (String[]) GET_APPLICATION_PROTOCOLS.invoke(sslSocket.getSSLParameters()); 283 if (Arrays.equals(protocolNames, configuredProtocols)) { 284 return; 285 } 286 } 287 } catch (IllegalAccessException e) { 288 throw new RuntimeException(e); 289 } catch (InvocationTargetException e) { 290 throw new RuntimeException(e); 291 } catch (InstantiationException e) { 292 throw new RuntimeException(e); 293 } 294 295 Object[] parameters = {Platform.concatLengthPrefixed(protocols)}; 296 if (platform.getTlsExtensionType() == TlsExtensionType.ALPN_AND_NPN) { 297 SET_ALPN_PROTOCOLS.invokeWithoutCheckedException(sslSocket, parameters); 298 } 299 if (platform.getTlsExtensionType() != TlsExtensionType.NONE) { 300 SET_NPN_PROTOCOLS.invokeWithoutCheckedException(sslSocket, parameters); 301 } else { 302 throw new RuntimeException("We can not do TLS handshake on this Android version, please" 303 + " install the Google Play Services Dynamic Security Provider to use TLS"); 304 } 305 } 306 307 @Override getSelectedProtocol(SSLSocket socket)308 public String getSelectedProtocol(SSLSocket socket) { 309 if (GET_APPLICATION_PROTOCOL != null) { 310 try { 311 return (String) GET_APPLICATION_PROTOCOL.invoke(socket); 312 } catch (IllegalAccessException e) { 313 throw new RuntimeException(e); 314 } catch (InvocationTargetException e) { 315 Throwable targetException = e.getTargetException(); 316 if (targetException instanceof UnsupportedOperationException) { 317 logger.log( 318 Level.FINER, 319 "Socket unsupported for getApplicationProtocol, will try old methods"); 320 } else { 321 throw new RuntimeException(e); 322 } 323 } 324 } 325 326 if (platform.getTlsExtensionType() == TlsExtensionType.ALPN_AND_NPN) { 327 try { 328 byte[] alpnResult = 329 (byte[]) GET_ALPN_SELECTED_PROTOCOL.invokeWithoutCheckedException(socket); 330 if (alpnResult != null) { 331 return new String(alpnResult, Util.UTF_8); 332 } 333 } catch (Exception e) { 334 logger.log(Level.FINE, "Failed calling getAlpnSelectedProtocol()", e); 335 // In some implementations, querying selected protocol before the handshake will fail with 336 // exception. 337 } 338 } 339 340 if (platform.getTlsExtensionType() != TlsExtensionType.NONE) { 341 try { 342 byte[] npnResult = 343 (byte[]) GET_NPN_SELECTED_PROTOCOL.invokeWithoutCheckedException(socket); 344 if (npnResult != null) { 345 return new String(npnResult, Util.UTF_8); 346 } 347 } catch (Exception e) { 348 logger.log(Level.FINE, "Failed calling getNpnSelectedProtocol()", e); 349 // In some implementations, querying selected protocol before the handshake will fail with 350 // exception. 351 } 352 } 353 return null; 354 } 355 } 356 protocolIds(List<Protocol> protocols)357 private static String[] protocolIds(List<Protocol> protocols) { 358 List<String> result = new ArrayList<>(); 359 for (Protocol protocol : protocols) { 360 result.add(protocol.toString()); 361 } 362 return result.toArray(new String[0]); 363 } 364 365 @VisibleForTesting isValidHostName(String name)366 static boolean isValidHostName(String name) { 367 // GrpcUtil.checkAuthority() depends on URI implementation, while Android's URI implementation 368 // allows underscore in hostname. Manually disallow hostname with underscore to avoid troubles. 369 // See b/154375837. 370 if (name.contains("_")) { 371 return false; 372 } 373 try { 374 GrpcUtil.checkAuthority(name); 375 return true; 376 } catch (IllegalArgumentException e) { 377 return false; 378 } 379 } 380 } 381