• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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