Skip to content

Commit 085857f

Browse files
kryalamatrask
andauthored
Friendly exception for cipher suite issue (#2053)
* friendly exception for cipher suites issue * address comments * fix spotbug issues * update ssl exception message to show only missed ciphers * Update agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/common/NetworkFriendlyExceptions.java Co-authored-by: Trask Stalnaker <[email protected]> * handle sslhandshaketimeout exception and update missing ciphers message * address comments * adding unit tests * fix checkstyle issues * fix style issues * address comments Co-authored-by: Trask Stalnaker <[email protected]>
1 parent 19713f1 commit 085857f

File tree

2 files changed

+250
-43
lines changed

2 files changed

+250
-43
lines changed

agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/common/NetworkFriendlyExceptions.java

Lines changed: 181 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -22,32 +22,60 @@
2222
package com.microsoft.applicationinsights.agent.internal.common;
2323

2424
import com.microsoft.applicationinsights.agent.internal.configuration.DefaultEndpoints;
25+
import io.netty.handler.ssl.SslHandshakeTimeoutException;
2526
import java.io.File;
27+
import java.io.IOException;
2628
import java.net.UnknownHostException;
29+
import java.security.NoSuchAlgorithmException;
30+
import java.util.ArrayList;
31+
import java.util.Arrays;
32+
import java.util.List;
2733
import java.util.concurrent.atomic.AtomicBoolean;
34+
import javax.net.ssl.SSLContext;
2835
import javax.net.ssl.SSLHandshakeException;
36+
import javax.net.ssl.SSLSocketFactory;
2937
import org.checkerframework.checker.nullness.qual.Nullable;
3038
import org.slf4j.Logger;
39+
import org.slf4j.LoggerFactory;
3140

3241
public class NetworkFriendlyExceptions {
3342

43+
private static final List<FriendlyExceptionDetector> DETECTORS;
44+
private static final Logger logger = LoggerFactory.getLogger(NetworkFriendlyExceptions.class);
45+
46+
static {
47+
DETECTORS = new ArrayList<>();
48+
// Note this order is important to determine the right exception!
49+
// For example SSLHandshakeException extends IOException
50+
DETECTORS.add(SslExceptionDetector.create());
51+
DETECTORS.add(UnknownHostExceptionDetector.create());
52+
try {
53+
DETECTORS.add(CipherExceptionDetector.create());
54+
} catch (NoSuchAlgorithmException e) {
55+
logger.debug(e.getMessage(), e);
56+
}
57+
}
58+
3459
// returns true if the exception was "handled" and the caller should not log it
3560
public static boolean logSpecialOneTimeFriendlyException(
3661
Throwable error, String url, AtomicBoolean alreadySeen, Logger logger) {
37-
// Handle SSL cert exceptions
38-
SSLHandshakeException sslException = getCausedByOfType(error, SSLHandshakeException.class);
39-
if (sslException != null) {
40-
if (!alreadySeen.getAndSet(true)) {
41-
logger.error(getSslFriendlyMessage(url));
62+
return logSpecialOneTimeFriendlyException(error, url, alreadySeen, logger, DETECTORS);
63+
}
64+
65+
public static boolean logSpecialOneTimeFriendlyException(
66+
Throwable error,
67+
String url,
68+
AtomicBoolean alreadySeen,
69+
Logger logger,
70+
List<FriendlyExceptionDetector> detectors) {
71+
72+
for (FriendlyExceptionDetector detector : detectors) {
73+
if (detector.detect(error)) {
74+
if (!alreadySeen.getAndSet(true)) {
75+
logger.error(detector.message(url));
76+
}
77+
return true;
4278
}
43-
return true;
44-
}
45-
UnknownHostException unknownHostException =
46-
getCausedByOfType(error, UnknownHostException.class);
47-
if (unknownHostException != null && !alreadySeen.getAndSet(true)) {
48-
// TODO log friendly message with instructions how to troubleshoot
49-
// e.g. wrong host address or cannot reach address due to network issues...
50-
return false;
5179
}
5280
return false;
5381
}
@@ -65,51 +93,161 @@ private static <T extends Exception> T getCausedByOfType(Throwable throwable, Cl
6593
return getCausedByOfType(cause, type);
6694
}
6795

68-
private static String getSslFriendlyMessage(String url) {
69-
return FriendlyException.populateFriendlyMessage(
70-
getSslFriendlyExceptionBanner(url),
71-
getSslFriendlyExceptionAction(url),
72-
"Unable to find valid certification path to requested target.",
73-
"This message is only logged the first time it occurs after startup.");
74-
}
75-
76-
private static String getSslFriendlyExceptionBanner(String url) {
77-
if (url.equals(DefaultEndpoints.LIVE_ENDPOINT)) {
96+
private static String getFriendlyExceptionBanner(String url) {
97+
if (url.contains(DefaultEndpoints.LIVE_ENDPOINT)) {
7898
return "ApplicationInsights Java Agent failed to connect to Live metric end point.";
7999
}
80100
return "ApplicationInsights Java Agent failed to send telemetry data.";
81101
}
82102

83-
private static String getSslFriendlyExceptionAction(String url) {
84-
String customJavaKeyStorePath = getCustomJavaKeystorePath();
85-
if (customJavaKeyStorePath != null) {
103+
interface FriendlyExceptionDetector {
104+
boolean detect(Throwable error);
105+
106+
String message(String url);
107+
}
108+
109+
static class SslExceptionDetector implements FriendlyExceptionDetector {
110+
111+
static SslExceptionDetector create() {
112+
return new SslExceptionDetector();
113+
}
114+
115+
@Override
116+
public boolean detect(Throwable error) {
117+
if (error instanceof SslHandshakeTimeoutException) {
118+
return false;
119+
}
120+
SSLHandshakeException sslException = getCausedByOfType(error, SSLHandshakeException.class);
121+
return sslException != null;
122+
}
123+
124+
@Override
125+
public String message(String url) {
126+
return FriendlyException.populateFriendlyMessage(
127+
"Unable to find valid certification path to requested target.",
128+
getSslFriendlyExceptionAction(url),
129+
getFriendlyExceptionBanner(url),
130+
"This message is only logged the first time it occurs after startup.");
131+
}
132+
133+
private static String getJavaCacertsPath() {
134+
String javaHome = System.getProperty("java.home");
135+
return new File(javaHome, "lib/security/cacerts").getPath();
136+
}
137+
138+
@Nullable
139+
private static String getCustomJavaKeystorePath() {
140+
String cacertsPath = System.getProperty("javax.net.ssl.trustStore");
141+
if (cacertsPath != null) {
142+
return new File(cacertsPath).getPath();
143+
}
144+
return null;
145+
}
146+
147+
private static String getSslFriendlyExceptionAction(String url) {
148+
String customJavaKeyStorePath = getCustomJavaKeystorePath();
149+
if (customJavaKeyStorePath != null) {
150+
return "Please import the SSL certificate from "
151+
+ url
152+
+ ", into your custom java key store located at:\n"
153+
+ customJavaKeyStorePath
154+
+ "\n"
155+
+ "Learn more about importing the certificate here: https://go.microsoft.com/fwlink/?linkid=2151450";
156+
}
86157
return "Please import the SSL certificate from "
87158
+ url
88-
+ ", into your custom java key store located at:\n"
89-
+ customJavaKeyStorePath
159+
+ ", into the default java key store located at:\n"
160+
+ getJavaCacertsPath()
90161
+ "\n"
91162
+ "Learn more about importing the certificate here: https://go.microsoft.com/fwlink/?linkid=2151450";
92163
}
93-
return "Please import the SSL certificate from "
94-
+ url
95-
+ ", into the default java key store located at:\n"
96-
+ getJavaCacertsPath()
97-
+ "\n"
98-
+ "Learn more about importing the certificate here: https://go.microsoft.com/fwlink/?linkid=2151450";
99164
}
100165

101-
private static String getJavaCacertsPath() {
102-
String javaHome = System.getProperty("java.home");
103-
return new File(javaHome, "lib/security/cacerts").getPath();
166+
static class UnknownHostExceptionDetector implements FriendlyExceptionDetector {
167+
168+
static UnknownHostExceptionDetector create() {
169+
return new UnknownHostExceptionDetector();
170+
}
171+
172+
@Override
173+
public boolean detect(Throwable error) {
174+
UnknownHostException unknownHostException =
175+
getCausedByOfType(error, UnknownHostException.class);
176+
return unknownHostException != null;
177+
}
178+
179+
@Override
180+
public String message(String url) {
181+
return FriendlyException.populateFriendlyMessage(
182+
"Unable to resolve host in end point url",
183+
getUnknownHostFriendlyExceptionAction(url),
184+
getFriendlyExceptionBanner(url),
185+
"This message is only logged the first time it occurs after startup.");
186+
}
187+
188+
private static String getUnknownHostFriendlyExceptionAction(String url) {
189+
return "Please upgrade your network to resolve the host in url :"
190+
+ url
191+
+ "\nLearn more about troubleshooting unknown host exception here: https://go.microsoft.com/fwlink/?linkid=2185830";
192+
}
104193
}
105194

106-
@Nullable
107-
private static String getCustomJavaKeystorePath() {
108-
String cacertsPath = System.getProperty("javax.net.ssl.trustStore");
109-
if (cacertsPath != null) {
110-
return new File(cacertsPath).getPath();
195+
static class CipherExceptionDetector implements FriendlyExceptionDetector {
196+
197+
private static final List<String> EXPECTED_CIPHERS =
198+
Arrays.asList(
199+
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
200+
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
201+
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
202+
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256");
203+
private final List<String> cipherSuitesFromJvm;
204+
205+
static CipherExceptionDetector create() throws NoSuchAlgorithmException {
206+
SSLSocketFactory socketFactory = SSLContext.getDefault().getSocketFactory();
207+
return new CipherExceptionDetector(Arrays.asList(socketFactory.getSupportedCipherSuites()));
208+
}
209+
210+
CipherExceptionDetector(List<String> cipherSuitesFromJvm) {
211+
this.cipherSuitesFromJvm = cipherSuitesFromJvm;
212+
}
213+
214+
@Override
215+
public boolean detect(Throwable error) {
216+
IOException exception = getCausedByOfType(error, IOException.class);
217+
if (exception == null) {
218+
return false;
219+
}
220+
for (String cipher : EXPECTED_CIPHERS) {
221+
if (cipherSuitesFromJvm.contains(cipher)) {
222+
return false;
223+
}
224+
}
225+
return true;
226+
}
227+
228+
@Override
229+
public String message(String url) {
230+
return FriendlyException.populateFriendlyMessage(
231+
"Probable root cause may be : missing cipher suites which are expected by the requested target.",
232+
getCipherFriendlyExceptionAction(url),
233+
getFriendlyExceptionBanner(url),
234+
"This message is only logged the first time it occurs after startup.");
235+
}
236+
237+
private static String getCipherFriendlyExceptionAction(String url) {
238+
StringBuilder actionBuilder = new StringBuilder();
239+
actionBuilder
240+
.append(
241+
"The Application Insights Java agent detects that you do not have any of the following cipher suites that are supported by the endpoint it connects to: "
242+
+ url)
243+
.append("\n");
244+
for (String missingCipher : EXPECTED_CIPHERS) {
245+
actionBuilder.append(missingCipher).append("\n");
246+
}
247+
actionBuilder.append(
248+
"Learn more about troubleshooting this network issue related to cipher suites here: https://go.microsoft.com/fwlink/?linkid=2185426");
249+
return actionBuilder.toString();
111250
}
112-
return null;
113251
}
114252

115253
private NetworkFriendlyExceptions() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* ApplicationInsights-Java
3+
* Copyright (c) Microsoft Corporation
4+
* All rights reserved.
5+
*
6+
* MIT License
7+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
8+
* software and associated documentation files (the ""Software""), to deal in the Software
9+
* without restriction, including without limitation the rights to use, copy, modify, merge,
10+
* publish, distribute, sublicense, and/or sell copies of the Software, and to permit
11+
* persons to whom the Software is furnished to do so, subject to the following conditions:
12+
* The above copyright notice and this permission notice shall be included in all copies or
13+
* substantial portions of the Software.
14+
* THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15+
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
16+
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
17+
* FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
18+
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19+
* DEALINGS IN THE SOFTWARE.
20+
*/
21+
22+
package com.microsoft.applicationinsights.agent.internal.common;
23+
24+
import static org.assertj.core.api.Assertions.assertThat;
25+
26+
import java.io.IOException;
27+
import java.net.UnknownHostException;
28+
import java.util.ArrayList;
29+
import java.util.Arrays;
30+
import java.util.List;
31+
import javax.net.ssl.SSLHandshakeException;
32+
import org.junit.jupiter.api.Test;
33+
34+
public class NetworkFriendlyExceptionsTest {
35+
36+
@Test
37+
public void testCipherExceptionDetectorWithNoCiphers() {
38+
Exception ioException = new IOException();
39+
List<String> existingCiphers = new ArrayList<>();
40+
NetworkFriendlyExceptions.CipherExceptionDetector cipherExceptionDetector =
41+
new NetworkFriendlyExceptions.CipherExceptionDetector(existingCiphers);
42+
assertThat(cipherExceptionDetector.detect(ioException)).isEqualTo(true);
43+
}
44+
45+
@Test
46+
public void testCipherExceptionDetectorWithCiphers() {
47+
Exception ioException = new IOException();
48+
List<String> existingCiphers = Arrays.asList("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384");
49+
NetworkFriendlyExceptions.CipherExceptionDetector cipherExceptionDetector =
50+
new NetworkFriendlyExceptions.CipherExceptionDetector(existingCiphers);
51+
assertThat(cipherExceptionDetector.detect(ioException)).isEqualTo(false);
52+
}
53+
54+
@Test
55+
public void testSslExceptionDetector() {
56+
Exception sslException = new SSLHandshakeException("sample");
57+
NetworkFriendlyExceptions.SslExceptionDetector sslExceptionDetector =
58+
new NetworkFriendlyExceptions.SslExceptionDetector();
59+
assertThat(sslExceptionDetector.detect(sslException)).isEqualTo(true);
60+
}
61+
62+
@Test
63+
public void testUnknownHostExceptionDetector() {
64+
Exception unknownHostException = new UnknownHostException("sample");
65+
NetworkFriendlyExceptions.UnknownHostExceptionDetector unknownHostExceptionDetector =
66+
new NetworkFriendlyExceptions.UnknownHostExceptionDetector();
67+
assertThat(unknownHostExceptionDetector.detect(unknownHostException)).isEqualTo(true);
68+
}
69+
}

0 commit comments

Comments
 (0)