Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2021-2026 Open Text.
*
* The only warranties for products and services of Open Text
* and its affiliates and licensors ("Open Text") are as may
* be set forth in the express warranty statements accompanying
* such products and services. Nothing herein should be construed
* as constituting an additional warranty. Open Text shall not be
* liable for technical or editorial errors or omissions contained
* herein. The information contained herein is subject to change
* without notice.
*/
package com.fortify.cli.aviator._common.exception;

/**
* User-facing rejection of a non-{@code https} Aviator target scheme.
* <p>
* Typed so diagnose can map endpoint UX without parsing exception message text.
*/
public class UnsupportedAviatorUrlSchemeException extends AviatorSimpleException {
private static final long serialVersionUID = 1L;

public static final String STAGE_SUMMARY = "Unsupported URL scheme";
public static final String STAGE_GUIDANCE = "Use a supported Aviator URL (https://host[:port])";

private final String scheme;
private final String providedUrl;

public UnsupportedAviatorUrlSchemeException(String scheme, String providedUrl) {
super(STAGE_SUMMARY+" '"+scheme+"'. "+STAGE_GUIDANCE+". Provided URL: "+providedUrl);
this.scheme = scheme;
this.providedUrl = providedUrl;
}

public String getScheme() {
return scheme;
}

public String getProvidedUrl() {
return providedUrl;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
import java.net.InetAddress;
import java.util.Arrays;
import java.util.List;
import java.util.StringJoiner;

import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fortify.cli.aviator._common.exception.AviatorBugException;
import com.fortify.cli.aviator._common.exception.AviatorSimpleException;
import com.fortify.cli.aviator._common.exception.UnsupportedAviatorUrlSchemeException;
import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper;
import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper.AviatorConnectionPlan;
import com.fortify.cli.common.json.JsonHelper;
Expand All @@ -44,14 +46,14 @@ public AviatorConnectionDiagnostics(IAviatorDiagnosticProbe probe) {
}

public AviatorDiagnosticReport diagnose(String url, int timeoutSeconds, String sourceType) {
var report = new AviatorDiagnosticReport();
report.begin(AviatorDiagnosticStage.ENDPOINT);
try {
return diagnose(AviatorGrpcClientHelper.createConnectionPlan(url), timeoutSeconds, sourceType);
return diagnoseValidated(report, AviatorGrpcClientHelper.createConnectionPlan(url), timeoutSeconds,
sourceType);
} catch (AviatorSimpleException e) {
var report = new AviatorDiagnosticReport();
report.fail(AviatorDiagnosticStage.ENDPOINT,
"Endpoint is invalid", "Use a valid Aviator host name and optional port",
AviatorDiagnosticEvidence.errorEvidence(e));
skipAfter(report, null, AviatorDiagnosticStage.ENDPOINT, "endpoint configuration failed");
failEndpoint(report, e);
skipAfter(report, null, AviatorDiagnosticStage.ENDPOINT, "endpoint validation failed");
return report;
}
}
Expand All @@ -61,8 +63,15 @@ public AviatorDiagnosticReport diagnose(String url, int timeoutSeconds, String s
*/
public AviatorDiagnosticReport diagnose(AviatorConnectionPlan connectionPlan, int timeoutSeconds, String sourceType) {
var report = new AviatorDiagnosticReport();
report.begin(AviatorDiagnosticStage.ENDPOINT);
return diagnoseValidated(report, connectionPlan, timeoutSeconds, sourceType);
}

private AviatorDiagnosticReport diagnoseValidated(AviatorDiagnosticReport report,
AviatorConnectionPlan connectionPlan, int timeoutSeconds, String sourceType) {
report.pass(AviatorDiagnosticStage.ENDPOINT,
"Endpoint is valid", "No action required", endpointEvidence(connectionPlan, sourceType));
"Endpoint is valid: "+connectionPlan.normalizedUrl(), "No action required",
endpointEvidence(connectionPlan, sourceType));

if (!runDns(report, connectionPlan)) {
skipAfter(report, connectionPlan, AviatorDiagnosticStage.DNS, "DNS resolution failed");
Expand All @@ -79,15 +88,32 @@ public AviatorDiagnosticReport diagnose(AviatorConnectionPlan connectionPlan, in
return report;
}

private static void failEndpoint(AviatorDiagnosticReport report, AviatorSimpleException e) {
var evidence = AviatorDiagnosticEvidence.errorEvidence(e);
if (e instanceof UnsupportedAviatorUrlSchemeException schemeFailure) {
evidence.put("scheme", schemeFailure.getScheme());
evidence.put("providedUrl", schemeFailure.getProvidedUrl());
report.fail(AviatorDiagnosticStage.ENDPOINT,
UnsupportedAviatorUrlSchemeException.STAGE_SUMMARY,
UnsupportedAviatorUrlSchemeException.STAGE_GUIDANCE,
evidence);
return;
}
report.fail(AviatorDiagnosticStage.ENDPOINT,
"Endpoint is invalid", "Use a valid Aviator host name and optional port", evidence);
}

private boolean runDns(AviatorDiagnosticReport report, AviatorConnectionPlan connectionPlan) {
report.begin(AviatorDiagnosticStage.DNS);
try {
var evidence = JsonHelper.getObjectMapper().createObjectNode();
addAddresses(evidence, "resolvedAddresses", probe.resolve(connectionPlan.target().host()));
if (connectionPlan.proxyDescriptor().isPresent()) {
var proxy = connectionPlan.proxyDescriptor().get();
addAddresses(evidence, "proxyResolvedAddresses", probe.resolve(proxy.getProxyHost()));
}
report.pass(AviatorDiagnosticStage.DNS, "Host name resolved", "No action required", evidence);
report.pass(AviatorDiagnosticStage.DNS,
"Host name resolved: "+addresses(evidence, "resolvedAddresses"), "No action required", evidence);
return true;
} catch (IOException e) {
report.fail(AviatorDiagnosticStage.DNS,
Expand All @@ -105,13 +131,15 @@ private boolean runDns(AviatorDiagnosticReport report, AviatorConnectionPlan con
* handshake failure. Do not fold TCP into the tunnel solely to avoid a double connect.
*/
private boolean runTcp(AviatorDiagnosticReport report, AviatorConnectionPlan connectionPlan, int timeoutSeconds) {
report.begin(AviatorDiagnosticStage.TCP);
var proxyDescriptor = connectionPlan.proxyDescriptor();
var nextHopHost = proxyDescriptor.map(proxy -> proxy.getProxyHost()).orElse(connectionPlan.target().host());
var nextHopPort = proxyDescriptor.map(proxy -> proxy.getProxyPort()).orElse(connectionPlan.effectivePort());
var evidence = nextHopEvidence(nextHopHost, nextHopPort, proxyDescriptor.isPresent());
try {
probe.connect(nextHopHost, nextHopPort, timeoutSeconds);
report.pass(AviatorDiagnosticStage.TCP, "TCP connection opened", "No action required", evidence);
report.pass(AviatorDiagnosticStage.TCP,
"TCP connection opened to "+nextHopHost+":"+nextHopPort, "No action required", evidence);
return true;
} catch (Exception e) {
putError(evidence, e);
Expand All @@ -132,13 +160,19 @@ private boolean runTcp(AviatorDiagnosticReport report, AviatorConnectionPlan con
*/
private boolean runTunnelStages(AviatorDiagnosticReport report, AviatorConnectionPlan connectionPlan,
int timeoutSeconds) {
var hasProxy = connectionPlan.proxyDescriptor().isPresent();
// First stage that will be recorded from this shared tunnel session.
report.begin(hasProxy ? AviatorDiagnosticStage.PROXY : AviatorDiagnosticStage.TLS);
var tunnel = probe.probeTunnel(connectionPlan, timeoutSeconds);
if (tunnel instanceof AviatorTunnelResult.ProxyConnectFailed failed) {
appendProxyFailure(report, connectionPlan, failed);
skipAfter(report, connectionPlan, AviatorDiagnosticStage.PROXY, "proxy CONNECT failed");
return false;
}
appendProxyPassIfConfigured(report, connectionPlan, tunnel);
if (hasProxy) {
report.begin(AviatorDiagnosticStage.TLS);
}
if (tunnel instanceof AviatorTunnelResult.TlsFailed failed) {
appendTlsFailure(report, connectionPlan, failed);
skipAfter(report, connectionPlan, AviatorDiagnosticStage.TLS, "TLS handshake failed");
Expand Down Expand Up @@ -167,7 +201,8 @@ private void appendProxyPassIfConfigured(AviatorDiagnosticReport report,
var evidence = JsonHelper.getObjectMapper().createObjectNode();
evidence.put("proxyConnectStatus", tunnel.proxyConnectStatus());
putProxyEvidence(evidence, connectionPlan);
report.pass(AviatorDiagnosticStage.PROXY, "Proxy CONNECT succeeded", "No action required", evidence);
report.pass(AviatorDiagnosticStage.PROXY,
"Proxy CONNECT succeeded through "+proxyEndpoint(evidence), "No action required", evidence);
}

private void appendTlsSuccess(AviatorDiagnosticReport report, AviatorTunnelResult.TlsSucceeded ok) {
Expand All @@ -180,10 +215,11 @@ private void appendTlsSuccess(AviatorDiagnosticReport report, AviatorTunnelResul
evidence.put("tlsPhase", AviatorTlsPhase.HANDSHAKE.id());
if (!"h2".equals(ok.applicationProtocol())) {
report.warn(AviatorDiagnosticStage.TLS,
"TLS works, but HTTP/2 was not enabled",
tlsSummary("TLS works, but HTTP/2 was not enabled", ok),
"Allow ALPN h2 through the proxy or gateway to aviator-grpc-server", true, evidence);
} else {
report.pass(AviatorDiagnosticStage.TLS, "TLS and HTTP/2 are available", "No action required", evidence);
report.pass(AviatorDiagnosticStage.TLS,
tlsSummary("TLS and HTTP/2 are available", ok), "No action required", evidence);
}
}

Expand All @@ -207,6 +243,7 @@ private void appendTlsFailure(AviatorDiagnosticReport report, AviatorConnectionP
}

private void runGrpc(AviatorDiagnosticReport report, AviatorConnectionPlan connectionPlan, int timeoutSeconds) {
report.begin(AviatorDiagnosticStage.GRPC);
try {
applyGrpc(report, probe.probeGrpc(connectionPlan.originalUrl(), timeoutSeconds));
} catch (Exception e) {
Expand All @@ -220,10 +257,11 @@ private void applyGrpc(AviatorDiagnosticReport report, AviatorGrpcReachabilityRe
if (grpc.pattern() != null) {
evidence.put("pattern", grpc.pattern().wireId());
}
var summary = grpc.stageSummary();
if (grpc.stagePass()) {
report.pass(AviatorDiagnosticStage.GRPC, grpc.stageSummary(), grpc.stageGuidance(), evidence);
report.pass(AviatorDiagnosticStage.GRPC, summary, grpc.stageGuidance(), evidence);
} else {
report.fail(AviatorDiagnosticStage.GRPC, grpc.stageSummary(), grpc.stageGuidance(), evidence);
report.fail(AviatorDiagnosticStage.GRPC, summary, grpc.stageGuidance(), evidence);
}
}

Expand Down Expand Up @@ -275,7 +313,7 @@ private static void skipAfter(AviatorDiagnosticReport report, AviatorConnectionP
if (stage == AviatorDiagnosticStage.PROXY && !hasProxy) {
continue;
}
report.skipWarn(stage, "Skipped because " + reason,
report.skipWarn(stage, reason,
"Resolve the previous failed required stage first",
AviatorDiagnosticEvidence.empty());
}
Expand All @@ -285,4 +323,18 @@ private static void addAddresses(ObjectNode evidence, String fieldName, InetAddr
var array = evidence.putArray(fieldName);
Arrays.stream(addresses).map(InetAddress::getHostAddress).forEach(array::add);
}

private static String addresses(ObjectNode evidence, String fieldName) {
var result = new StringJoiner(", ");
evidence.withArray(fieldName).forEach(address -> result.add(address.asText()));
return result.toString();
}

private static String proxyEndpoint(ObjectNode evidence) {
return evidence.path("proxyHost").asText()+":"+evidence.path("proxyPort").asInt();
}

private static String tlsSummary(String summary, AviatorTunnelResult.TlsSucceeded result) {
return summary+": "+result.protocol()+", ALPN "+result.applicationProtocol();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,77 @@
import java.util.Collections;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fortify.cli.common.json.JsonHelper;

/**
* Owns stage order and collects diagnostic rows for one diagnose run.
* Owns stage order, collects diagnostic rows, and emits support log lines for one diagnose run.
* <p>
* Call {@link #begin(AviatorDiagnosticStage)} (or the string overload) before running a stage;
* {@code pass}/{@code fail}/{@code warn}/{@code skip*} record the row and log the outcome.
* Skip APIs take a structured {@code reason} so log formatting never parses summary English.
*/
public final class AviatorDiagnosticReport {
private static final Logger LOG = LoggerFactory.getLogger(AviatorDiagnosticReport.class);

private final List<AviatorDiagnosticStageResult> stages = new ArrayList<>();

public int nextOrder() {
return stages.size() + 1;
}

/** DEBUG start line before a stage is executed (not for pure skip rows). */
public void begin(AviatorDiagnosticStage stage) {
LOG.debug("Starting {} diagnostic", stage.displayName());
}

/** DEBUG start line for product stages (token/admin) that are not transport enums. */
public void begin(String stageId) {
LOG.debug("Starting {} diagnostic", AviatorDiagnosticStage.displayNameFor(stageId));
}

public void add(AviatorDiagnosticStageResult result) {
stages.add(result);
logStageOutcome(result, false, null);
}

private void addSkip(AviatorDiagnosticStageResult result, String reason) {
stages.add(result);
logStageOutcome(result, true, reason);
}

private static void logStageOutcome(AviatorDiagnosticStageResult result, boolean skipped, String skipReason) {
var name = AviatorDiagnosticStage.displayNameFor(result.stage());
if (skipped) {
LOG.info("{} skipped because {}", name, nullToEmpty(skipReason));
return;
}
switch (result.status()) {
case PASS -> LOG.info("{}: PASS - {}", name, nullToEmpty(result.summary()));
case FAIL -> LOG.error("{}: FAIL - {}", name, failDetail(result));
case WARN -> LOG.info("{}: WARN - {}", name, nullToEmpty(result.summary()));
}
}

private static String failDetail(AviatorDiagnosticStageResult result) {
var summary = nullToEmpty(result.summary());
var evidence = result.evidence();
if (evidence == null || !evidence.hasNonNull("exceptionMessage")) {
return summary;
}
var exceptionMessage = evidence.get("exceptionMessage").asText();
if (exceptionMessage == null || exceptionMessage.isBlank()) {
return summary;
}
return summary+" ("+exceptionMessage+")";
}

private static String nullToEmpty(String value) {
return value == null ? "" : value;
}

public void pass(AviatorDiagnosticStage stage, String summary, String guidance, ObjectNode evidence) {
Expand All @@ -43,11 +98,12 @@ public void fail(AviatorDiagnosticStage stage, String summary, String guidance,
}

/**
* Transport skip WARN. {@code required=true} documents the stage as part of the required
* pipeline; WARN itself never drives process exit (only required FAIL does).
* Required transport skip WARN. Builds summary {@code Skipped because {reason}} and logs
* as a skip without parsing summary text.
*/
public void skipWarn(AviatorDiagnosticStage stage, String summary, String guidance, ObjectNode evidence) {
add(AviatorDiagnosticStageResult.warn(nextOrder(), stage, summary, guidance, true, evidence));
public void skipWarn(AviatorDiagnosticStage stage, String reason, String guidance, ObjectNode evidence) {
addSkip(AviatorDiagnosticStageResult.warn(nextOrder(), stage, skipSummary(reason), guidance, true, evidence),
reason);
}

public void warn(AviatorDiagnosticStage stage, String summary, String guidance, boolean required,
Expand All @@ -63,8 +119,16 @@ public void optionalFail(String stage, String description, String summary, Strin
add(AviatorDiagnosticStageResult.optionalFail(nextOrder(), stage, description, summary, guidance, evidence));
}

public void optionalSkipWarn(String stage, String description, String summary, String guidance, ObjectNode evidence) {
add(AviatorDiagnosticStageResult.warn(nextOrder(), stage, description, summary, guidance, false, evidence));
/**
* Optional product-stage skip WARN. Builds summary {@code Skipped because {reason}}.
*/
public void optionalSkipWarn(String stage, String description, String reason, String guidance, ObjectNode evidence) {
addSkip(AviatorDiagnosticStageResult.warn(nextOrder(), stage, description, skipSummary(reason), guidance, false,
evidence), reason);
}

private static String skipSummary(String reason) {
return "Skipped because "+reason;
}

public List<AviatorDiagnosticStageResult> stages() {
Expand Down
Loading
Loading