• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #19581

09 Dec 2024 12:20PM UTC coverage: 88.577% (-0.008%) from 88.585%
#19581

push

github

web-flow
okhttp: Fix for ipv6 link local with scope (#11725) (#11729)

Co-authored-by: MV Shiva <speakupshiva@gmail.com>

33376 of 37680 relevant lines covered (88.58%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.07
/../okhttp/src/main/java/io/grpc/okhttp/OkHttpProtocolNegotiator.java
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 com.google.common.net.HostAndPort;
23
import com.google.common.net.InetAddresses;
24
import io.grpc.internal.GrpcUtil;
25
import io.grpc.okhttp.internal.OptionalMethod;
26
import io.grpc.okhttp.internal.Platform;
27
import io.grpc.okhttp.internal.Platform.TlsExtensionType;
28
import io.grpc.okhttp.internal.Protocol;
29
import io.grpc.okhttp.internal.Util;
30
import java.io.IOException;
31
import java.lang.reflect.Constructor;
32
import java.lang.reflect.InvocationTargetException;
33
import java.lang.reflect.Method;
34
import java.net.Socket;
35
import java.util.ArrayList;
36
import java.util.Arrays;
37
import java.util.Collections;
38
import java.util.List;
39
import java.util.logging.Level;
40
import java.util.logging.Logger;
41
import javax.annotation.Nullable;
42
import javax.net.ssl.SSLParameters;
43
import javax.net.ssl.SSLSocket;
44

45
/**
46
 * A helper class located in package com.squareup.okhttp.internal for TLS negotiation.
47
 */
48
class OkHttpProtocolNegotiator {
49
  private static final Logger logger = Logger.getLogger(OkHttpProtocolNegotiator.class.getName());
1✔
50
  private static final Platform DEFAULT_PLATFORM = Platform.get();
1✔
51
  private static OkHttpProtocolNegotiator NEGOTIATOR =
1✔
52
      createNegotiator(OkHttpProtocolNegotiator.class.getClassLoader());
1✔
53

54
  protected final Platform platform;
55

56
  @VisibleForTesting
57
  OkHttpProtocolNegotiator(Platform platform) {
1✔
58
    this.platform = checkNotNull(platform, "platform");
1✔
59
  }
1✔
60

61
  public static OkHttpProtocolNegotiator get() {
62
    return NEGOTIATOR;
1✔
63
  }
64

65
  /**
66
   * Creates corresponding negotiator according to whether on Android.
67
   */
68
  @VisibleForTesting
69
  static OkHttpProtocolNegotiator createNegotiator(ClassLoader loader) {
70
    boolean android = true;
1✔
71
    try {
72
      // Attempt to find Android 2.3+ APIs.
73
      loader.loadClass("com.android.org.conscrypt.OpenSSLSocketImpl");
1✔
74
    } catch (ClassNotFoundException e1) {
1✔
75
      logger.log(Level.FINE, "Unable to find Conscrypt. Skipping", e1);
1✔
76
      try {
77
        // Older platform before being unbundled.
78
        loader.loadClass("org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
1✔
79
      } catch (ClassNotFoundException e2) {
1✔
80
        logger.log(Level.FINE, "Unable to find any OpenSSLSocketImpl. Skipping", e2);
1✔
81
        android = false;
1✔
82
      }
1✔
83
    }
1✔
84
    return android
1✔
85
        ? new AndroidNegotiator(DEFAULT_PLATFORM)
1✔
86
        : new OkHttpProtocolNegotiator(DEFAULT_PLATFORM);
1✔
87
  }
88

89
  /**
90
   * Start and wait until the negotiation is done, returns the negotiated protocol.
91
   *
92
   * @throws IOException if an IO error was encountered during the handshake.
93
   * @throws RuntimeException if the negotiation completed, but no protocol was selected.
94
   */
95
  public String negotiate(
96
      SSLSocket sslSocket, String hostname, @Nullable List<Protocol> protocols) throws IOException {
97
    if (protocols != null) {
1✔
98
      configureTlsExtensions(sslSocket, hostname, protocols);
1✔
99
    }
100
    try {
101
      // Force handshake.
102
      sslSocket.startHandshake();
1✔
103

104
      String negotiatedProtocol = getSelectedProtocol(sslSocket);
1✔
105
      if (negotiatedProtocol == null) {
1✔
106
        throw new RuntimeException("TLS ALPN negotiation failed with protocols: " + protocols);
1✔
107
      }
108
      return negotiatedProtocol;
1✔
109
    } finally {
110
      platform.afterHandshake(sslSocket);
1✔
111
    }
112
  }
113

114
  /** Configure TLS extensions. */
115
  protected void configureTlsExtensions(
116
      SSLSocket sslSocket, String hostname, List<Protocol> protocols) {
117
    platform.configureTlsExtensions(sslSocket, hostname, protocols);
1✔
118
  }
1✔
119

120
  /** Returns the negotiated protocol, or null if no protocol was negotiated. */
121
  public String getSelectedProtocol(SSLSocket socket) {
122
    return platform.getSelectedProtocol(socket);
1✔
123
  }
124

125
  @VisibleForTesting
126
  static final class AndroidNegotiator extends OkHttpProtocolNegotiator {
127
    // setUseSessionTickets(boolean)
128
    private static final OptionalMethod<Socket> SET_USE_SESSION_TICKETS =
1✔
129
        new OptionalMethod<>(null, "setUseSessionTickets", Boolean.TYPE);
130
    // setHostname(String)
131
    private static final OptionalMethod<Socket> SET_HOSTNAME =
1✔
132
        new OptionalMethod<>(null, "setHostname", String.class);
133
    // byte[] getAlpnSelectedProtocol()
134
    private static final OptionalMethod<Socket> GET_ALPN_SELECTED_PROTOCOL =
1✔
135
        new OptionalMethod<>(byte[].class, "getAlpnSelectedProtocol");
136
    // setAlpnProtocol(byte[])
137
    private static final OptionalMethod<Socket> SET_ALPN_PROTOCOLS =
1✔
138
        new OptionalMethod<>(null, "setAlpnProtocols", byte[].class);
139
    // byte[] getNpnSelectedProtocol()
140
    private static final OptionalMethod<Socket> GET_NPN_SELECTED_PROTOCOL =
1✔
141
        new OptionalMethod<>(byte[].class, "getNpnSelectedProtocol");
142
    // setNpnProtocol(byte[])
143
    private static final OptionalMethod<Socket> SET_NPN_PROTOCOLS =
1✔
144
        new OptionalMethod<>(null, "setNpnProtocols", byte[].class);
145

146
    // Non-null on Android 10.0+.
147
    // SSLSockets.isSupportedSocket(SSLSocket)
148
    private static final Method SSL_SOCKETS_IS_SUPPORTED_SOCKET;
149
    // SSLSockets.setUseSessionTickets(SSLSocket, boolean)
150
    private static final Method SSL_SOCKETS_SET_USE_SESSION_TICKET;
151
    // SSLParameters.setApplicationProtocols(String[])
152
    private static final Method SET_APPLICATION_PROTOCOLS;
153
    // SSLParameters.getApplicationProtocols()
154
    private static final Method GET_APPLICATION_PROTOCOLS;
155
    // SSLSocket.getApplicationProtocol()
156
    private static final Method GET_APPLICATION_PROTOCOL;
157

158
    // Non-null on Android 7.0+.
159
    // SSLParameters.setServerNames(List<SNIServerName>)
160
    private static final Method SET_SERVER_NAMES;
161
    // SNIHostName(String)
162
    private static final Constructor<?> SNI_HOST_NAME;
163

164
    static {
165
      // Attempt to find Android 10.0+ APIs.
166
      Method setApplicationProtocolsMethod = null;
1✔
167
      Method getApplicationProtocolsMethod = null;
1✔
168
      Method getApplicationProtocolMethod = null;
1✔
169
      Method sslSocketsIsSupportedSocketMethod = null;
1✔
170
      Method sslSocketsSetUseSessionTicketsMethod = null;
1✔
171
      try {
172
        Class<?> sslParameters = SSLParameters.class;
1✔
173
        setApplicationProtocolsMethod =
1✔
174
            sslParameters.getMethod("setApplicationProtocols", String[].class);
1✔
175
        getApplicationProtocolsMethod = sslParameters.getMethod("getApplicationProtocols");
1✔
176
        getApplicationProtocolMethod = SSLSocket.class.getMethod("getApplicationProtocol");
1✔
177
        Class<?> sslSockets = Class.forName("android.net.ssl.SSLSockets");
×
178
        sslSocketsIsSupportedSocketMethod =
×
179
            sslSockets.getMethod("isSupportedSocket", SSLSocket.class);
×
180
        sslSocketsSetUseSessionTicketsMethod =
×
181
            sslSockets.getMethod("setUseSessionTickets", SSLSocket.class, boolean.class);
×
182
      } catch (ClassNotFoundException e) {
1✔
183
        logger.log(Level.FINER, "Failed to find Android 10.0+ APIs", e);
1✔
184
      } catch (NoSuchMethodException e) {
×
185
        logger.log(Level.FINER, "Failed to find Android 10.0+ APIs", e);
×
186
      }
1✔
187
      SET_APPLICATION_PROTOCOLS = setApplicationProtocolsMethod;
1✔
188
      GET_APPLICATION_PROTOCOLS = getApplicationProtocolsMethod;
1✔
189
      GET_APPLICATION_PROTOCOL = getApplicationProtocolMethod;
1✔
190
      SSL_SOCKETS_IS_SUPPORTED_SOCKET = sslSocketsIsSupportedSocketMethod;
1✔
191
      SSL_SOCKETS_SET_USE_SESSION_TICKET = sslSocketsSetUseSessionTicketsMethod;
1✔
192

193
      // Attempt to find Android 7.0+ APIs.
194
      Method setServerNamesMethod = null;
1✔
195
      Constructor<?> sniHostNameConstructor = null;
1✔
196
      try {
197
        setServerNamesMethod = SSLParameters.class.getMethod("setServerNames", List.class);
1✔
198
        sniHostNameConstructor =
1✔
199
            Class.forName("javax.net.ssl.SNIHostName").getConstructor(String.class);
1✔
200
      } catch (ClassNotFoundException e) {
×
201
        logger.log(Level.FINER, "Failed to find Android 7.0+ APIs", e);
×
202
      } catch (NoSuchMethodException e) {
×
203
        logger.log(Level.FINER, "Failed to find Android 7.0+ APIs", e);
×
204
      }
1✔
205
      SET_SERVER_NAMES = setServerNamesMethod;
1✔
206
      SNI_HOST_NAME = sniHostNameConstructor;
1✔
207
    }
1✔
208

209
    AndroidNegotiator(Platform platform) {
210
      super(platform);
1✔
211
    }
1✔
212

213
    @Override
214
    public String negotiate(SSLSocket sslSocket, String hostname, List<Protocol> protocols)
215
        throws IOException {
216
      // First check if a protocol has already been selected, since it's possible that the user
217
      // provided SSLSocketFactory has already done the handshake when creates the SSLSocket.
218
      String negotiatedProtocol = getSelectedProtocol(sslSocket);
1✔
219
      if (negotiatedProtocol == null) {
1✔
220
        negotiatedProtocol = super.negotiate(sslSocket, hostname, protocols);
×
221
      }
222
      return negotiatedProtocol;
×
223
    }
224

225
    /**
226
     * Override {@link Platform}'s configureTlsExtensions for Android older than 5.0, since OkHttp
227
     * (2.3+) only support such function for Android 5.0+.
228
     *
229
     * <p>Note: Prior to Android Q, the standard way of accessing some Conscrypt features was to
230
     * use reflection to call hidden APIs. Beginning in Q, there is public API for all of these
231
     * features. We attempt to use the public API where possible. Otherwise, fall back to use the
232
     * old reflective API.
233
     */
234
    @Override
235
    protected void configureTlsExtensions(
236
        SSLSocket sslSocket, String hostname, List<Protocol> protocols) {
237
      String[] protocolNames = protocolIds(protocols);
1✔
238
      SSLParameters sslParams = sslSocket.getSSLParameters();
1✔
239
      try {
240
        // Enable SNI and session tickets.
241
        // Hostname is normally validated in the builder (see checkAuthority) and it should
242
        // virtually always succeed. Check again here to avoid troubles (e.g., hostname with
243
        // underscore) enabling SNI, which works around cases where checkAuthority is disabled.
244
        // See b/154375837.
245
        if (hostname != null && isValidHostName(hostname)) {
1✔
246
          if (SSL_SOCKETS_IS_SUPPORTED_SOCKET != null
1✔
247
              && (boolean) SSL_SOCKETS_IS_SUPPORTED_SOCKET.invoke(null, sslSocket)) {
×
248
            SSL_SOCKETS_SET_USE_SESSION_TICKET.invoke(null, sslSocket, true);
×
249
          } else {
250
            SET_USE_SESSION_TICKETS.invokeOptionalWithoutCheckedException(sslSocket, true);
1✔
251
          }
252
          if (SET_SERVER_NAMES != null
1✔
253
              && SNI_HOST_NAME != null
254
              && !InetAddresses.isInetAddress(HostAndPort.fromString(hostname).getHost())) {
1✔
255
            SET_SERVER_NAMES
1✔
256
                .invoke(sslParams, Collections.singletonList(SNI_HOST_NAME.newInstance(hostname)));
1✔
257
          } else {
258
            SET_HOSTNAME.invokeOptionalWithoutCheckedException(sslSocket, hostname);
×
259
          }
260
        }
261
        boolean alpnEnabled = false;
1✔
262
        if (GET_APPLICATION_PROTOCOL != null) {
1✔
263
          try {
264
            // If calling SSLSocket.getApplicationProtocol() throws UnsupportedOperationException,
265
            // the underlying provider does not implement operations for enabling
266
            // ALPN in the fashion of SSLParameters.setApplicationProtocols(). Fall back to
267
            // use old hidden methods.
268
            GET_APPLICATION_PROTOCOL.invoke(sslSocket);
×
269
            SET_APPLICATION_PROTOCOLS.invoke(sslParams, (Object) protocolNames);
×
270
            alpnEnabled = true;
×
271
          } catch (InvocationTargetException e) {
1✔
272
            Throwable targetException = e.getTargetException();
1✔
273
            if (targetException instanceof UnsupportedOperationException) {
1✔
274
              logger.log(Level.FINER, "setApplicationProtocol unsupported, will try old methods");
1✔
275
            } else {
276
              throw e;
×
277
            }
278
          }
×
279
        }
280
        sslSocket.setSSLParameters(sslParams);
1✔
281
        // Check application protocols are configured correctly. If not, configure again with
282
        // old methods.
283
        // Workaround for Conscrypt bug: https://github.com/google/conscrypt/issues/832
284
        if (alpnEnabled && GET_APPLICATION_PROTOCOLS != null) {
1✔
285
          String[] configuredProtocols =
×
286
              (String[]) GET_APPLICATION_PROTOCOLS.invoke(sslSocket.getSSLParameters());
×
287
          if (Arrays.equals(protocolNames, configuredProtocols)) {
×
288
            return;
×
289
          }
290
        }
291
      } catch (IllegalAccessException e) {
×
292
        throw new RuntimeException(e);
×
293
      } catch (InvocationTargetException e) {
×
294
        throw new RuntimeException(e);
×
295
      } catch (InstantiationException e) {
×
296
        throw new RuntimeException(e);
×
297
      }
1✔
298

299
      Object[] parameters = {Platform.concatLengthPrefixed(protocols)};
1✔
300
      if (platform.getTlsExtensionType() == TlsExtensionType.ALPN_AND_NPN) {
1✔
301
        SET_ALPN_PROTOCOLS.invokeWithoutCheckedException(sslSocket, parameters);
1✔
302
      }
303
      if (platform.getTlsExtensionType() != TlsExtensionType.NONE) {
1✔
304
        SET_NPN_PROTOCOLS.invokeWithoutCheckedException(sslSocket, parameters);
1✔
305
      } else {
306
        throw new RuntimeException("We can not do TLS handshake on this Android version, please"
×
307
            + " install the Google Play Services Dynamic Security Provider to use TLS");
308
      }
309
    }
1✔
310

311
    @Override
312
    public String getSelectedProtocol(SSLSocket socket) {
313
      if (GET_APPLICATION_PROTOCOL != null) {
1✔
314
        try {
315
          return (String) GET_APPLICATION_PROTOCOL.invoke(socket);
×
316
        } catch (IllegalAccessException e) {
×
317
          throw new RuntimeException(e);
×
318
        } catch (InvocationTargetException e) {
1✔
319
          Throwable targetException = e.getTargetException();
1✔
320
          if (targetException instanceof UnsupportedOperationException) {
1✔
321
            logger.log(
1✔
322
                Level.FINER,
323
                "Socket unsupported for getApplicationProtocol, will try old methods");
324
          } else {
325
            throw new RuntimeException(e);
×
326
          }
327
        }
328
      }
329

330
      if (platform.getTlsExtensionType() == TlsExtensionType.ALPN_AND_NPN) {
1✔
331
        try {
332
          byte[] alpnResult =
1✔
333
              (byte[]) GET_ALPN_SELECTED_PROTOCOL.invokeWithoutCheckedException(socket);
1✔
334
          if (alpnResult != null) {
1✔
335
            return new String(alpnResult, Util.UTF_8);
1✔
336
          }
337
        } catch (Exception e) {
×
338
          logger.log(Level.FINE, "Failed calling getAlpnSelectedProtocol()", e);
×
339
          // In some implementations, querying selected protocol before the handshake will fail with
340
          // exception.
341
        }
1✔
342
      }
343

344
      if (platform.getTlsExtensionType() != TlsExtensionType.NONE) {
1✔
345
        try {
346
          byte[] npnResult =
1✔
347
              (byte[]) GET_NPN_SELECTED_PROTOCOL.invokeWithoutCheckedException(socket);
1✔
348
          if (npnResult != null) {
1✔
349
            return new String(npnResult, Util.UTF_8);
1✔
350
          }
351
        } catch (Exception e) {
×
352
          logger.log(Level.FINE, "Failed calling getNpnSelectedProtocol()", e);
×
353
          // In some implementations, querying selected protocol before the handshake will fail with
354
          // exception.
355
        }
1✔
356
      }
357
      return null;
1✔
358
    }
359
  }
360

361
  private static String[] protocolIds(List<Protocol> protocols) {
362
    List<String> result = new ArrayList<>();
1✔
363
    for (Protocol protocol : protocols) {
1✔
364
      result.add(protocol.toString());
1✔
365
    }
1✔
366
    return result.toArray(new String[0]);
1✔
367
  }
368

369
  @VisibleForTesting
370
  static boolean isValidHostName(String name) {
371
    // GrpcUtil.checkAuthority() depends on URI implementation, while Android's URI implementation
372
    // allows underscore in hostname. Manually disallow hostname with underscore to avoid troubles.
373
    // See b/154375837.
374
    if (name.contains("_")) {
1✔
375
      return false;
1✔
376
    }
377
    try {
378
      GrpcUtil.checkAuthority(name);
1✔
379
      return true;
1✔
380
    } catch (IllegalArgumentException e) {
×
381
      return false;
×
382
    }
383
  }
384
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc