diff --git a/compose/cryostat.yml b/compose/cryostat.yml
index 19aeaa863..a1f4145e6 100644
--- a/compose/cryostat.yml
+++ b/compose/cryostat.yml
@@ -14,6 +14,7 @@ services:
image: ${CRYOSTAT_IMAGE:-quay.io/cryostat/cryostat:3.0.0-snapshot}
volumes:
- ${XDG_RUNTIME_DIR}/podman/podman.sock:/run/user/1000/podman/podman.sock:Z
+ - jmxtls_cfg:/truststore:U
security_opt:
- label:disable
hostname: cryostat3
@@ -39,3 +40,7 @@ services:
retries: 3
start_period: 30s
timeout: 5s
+
+volumes:
+ jmxtls_cfg:
+ external: true
diff --git a/pom.xml b/pom.xml
index 0316b9c63..9c38d68ac 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,21 +16,26 @@
UTF-8
UTF-8
- 2.30.1
+ amd64
+
+ 2.30.2
1.16.1
2.13.0
5.2.1
3.13.0
1.7
- 0.3.21
+ 0.4.4
+ 3.25.2
9.37.3
1.19.7
quarkus-bom
io.quarkus.platform
3.2.9.Final
2.3.6
+ 4.1.101.Final
3.5.0
+ 3.7.1
4.8.4
4.8.4.0
@@ -44,6 +49,13 @@
+
+ io.netty
+ netty-bom
+ ${io.netty.version}
+ pom
+ import
+
${quarkus.platform.group-id}
${quarkus.platform.artifact-id}
@@ -165,6 +177,12 @@
org.projectnessie.cel
cel-jackson
+
+
+ com.google.protobuf
+ protobuf-java
+ ${com.google.protobuf-java.version}
+
commons-validator
commons-validator
@@ -187,16 +205,6 @@
io.quarkus
quarkus-quartz
-
- io.netty
- netty-transport-native-epoll
- linux-x86_64
-
-
- io.netty
- netty-transport-native-kqueue
- osx-x86_64
-
com.google.googlejavaformat
google-java-format
@@ -418,5 +426,111 @@
native
+
+
+ default-arch
+
+
+ !build.arch
+
+
+
+ linux-x86_64
+ compile
+
+
+
+ amd64
+
+
+ build.arch
+ amd64
+
+
+
+ linux-x86_64
+ compile
+
+
+
+ arm64
+
+
+ build.arch
+ arm64
+
+
+
+ linux-aarch_64
+ compile
+
+
+
+ with-epoll
+
+
+ !build.exclude-epoll
+
+
+
+ compile
+
+
+
+ io.netty
+ netty-transport-native-epoll
+ ${io.netty.version}
+ ${io.netty.netty-transport-native-epoll.classifier}
+ ${io.netty.netty-transport-native-epoll.scope}
+
+
+
+
+ no-epoll
+
+
+ build.exclude-epoll
+
+
+
+ linux-x86_64
+ provided
+
+
+
+ io.netty
+ netty-transport-native-epoll
+ ${io.netty.netty-transport-native-epoll.classifier}
+ ${io.netty.netty-transport-native-epoll.scope}
+
+
+
+
+
+ dist
+
+
+
+ maven-assembly-plugin
+ ${assembly-plugin.version}
+
+
+ src/assembly/quarkus-app.xml
+
+ posix
+
+
+
+ assemble-quarkus-app
+ package
+
+ single
+
+
+
+
+
+
+
diff --git a/schema/openapi.yaml b/schema/openapi.yaml
index d4252a23e..3290d9499 100644
--- a/schema/openapi.yaml
+++ b/schema/openapi.yaml
@@ -2236,6 +2236,19 @@ paths:
- SecurityScheme: []
tags:
- Reports
+ /api/v3/tls/certs:
+ get:
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ items:
+ type: string
+ type: array
+ description: OK
+ tags:
+ - Trust Store
/health:
get:
responses:
diff --git a/smoketest.bash b/smoketest.bash
index 6e88e733a..9e77c6478 100755
--- a/smoketest.bash
+++ b/smoketest.bash
@@ -180,6 +180,8 @@ cleanup() {
${container_engine} rm localstack_cfg_helper || true
${container_engine} volume rm localstack_cfg || true
fi
+ ${container_engine} rm jmxtls_cfg_helper || true
+ ${container_engine} volume rm jmxtls_cfg || true
truncate -s 0 "${HOSTSFILE}"
for i in "${PIDS[@]}"; do
kill -0 "${i}" && kill "${i}"
@@ -212,6 +214,13 @@ if [ "${s3}" = "localstack" ]; then
createLocalstackCfgVolume
fi
+createJmxTlsCertVolume() {
+ "${container_engine}" volume create jmxtls_cfg
+ "${container_engine}" container create --name jmxtls_cfg_helper -v jmxtls_cfg:/truststore busybox
+ "${container_engine}" cp "${DIR}/truststore" jmxtls_cfg_helper:/truststore
+}
+createJmxTlsCertVolume
+
setupUserHosts() {
# This requires https://github.com/figiel/hosts to work. See README.
truncate -s 0 "${HOSTSFILE}"
diff --git a/src/assembly/quarkus-app.xml b/src/assembly/quarkus-app.xml
new file mode 100644
index 000000000..8b55a03eb
--- /dev/null
+++ b/src/assembly/quarkus-app.xml
@@ -0,0 +1,14 @@
+
+ quarkus-app
+
+ tar.gz
+
+ false
+
+
+ ${project.build.directory}/quarkus-app
+
+
+
diff --git a/src/main/docker/Dockerfile.jvm b/src/main/docker/Dockerfile.jvm
index 8f5b902da..db1431403 100644
--- a/src/main/docker/Dockerfile.jvm
+++ b/src/main/docker/Dockerfile.jvm
@@ -88,11 +88,26 @@ LABEL io.cryostat.component=cryostat3
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
-ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]
+ENTRYPOINT [ "/deployments/app/entrypoint.bash", "/opt/jboss/container/java/run/run-java.sh" ]
# We make distinct layers so if there are application changes the library layers can be re-used
COPY --chown=185 src/main/docker/include/cryostat.jfc /usr/lib/jvm/jre/lib/jfr/
+COPY --chown=185 src/main/docker/include/genpass.bash /deployments/app/
+COPY --chown=185 src/main/docker/include/entrypoint.bash /deployments/app/
+COPY --chown=185 src/main/docker/include/truststore-setup.bash /deployments/app/
COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/
COPY --chown=185 target/quarkus-app/*.jar /deployments/
COPY --chown=185 target/quarkus-app/app/ /deployments/app/
COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/
+
+ENV CONF_DIR=/opt/cryostat.d
+ENV SSL_TRUSTSTORE=$CONF_DIR/truststore.p12 \
+ SSL_TRUSTSTORE_PASS_FILE=$CONF_DIR/truststore.pass
+
+USER root
+RUN mkdir -p $CONF_DIR \
+ && chmod -R g=u $CONF_DIR \
+ && chown jboss:root $CONF_DIR
+USER 185
+
+RUN /deployments/app/truststore-setup.bash
diff --git a/src/main/docker/include/entrypoint.bash b/src/main/docker/include/entrypoint.bash
new file mode 100755
index 000000000..892f4c71a
--- /dev/null
+++ b/src/main/docker/include/entrypoint.bash
@@ -0,0 +1,82 @@
+#!/bin/bash
+
+set -e
+
+DIR="$(dirname "$(realpath "$0")")"
+source "${DIR}/genpass.bash"
+
+function banner() {
+ echo "+------------------------------------------+"
+ printf "| %-40s |\n" "$(date)"
+ echo "| |"
+ printf "| %-40s |\n" "$@"
+ echo "+------------------------------------------+"
+}
+
+PWFILE="/tmp/jmxremote.password"
+USRFILE="/tmp/jmxremote.access"
+function createJmxCredentials() {
+ if [ -z "$CRYOSTAT_RJMX_USER" ]; then
+ CRYOSTAT_RJMX_USER="cryostat"
+ fi
+ if [ -z "$CRYOSTAT_RJMX_PASS" ]; then
+ CRYOSTAT_RJMX_PASS="$(genpass)"
+ fi
+
+ echo -n "$CRYOSTAT_RJMX_USER $CRYOSTAT_RJMX_PASS" > "$PWFILE"
+ chmod 400 "$PWFILE"
+ echo -n "$CRYOSTAT_RJMX_USER readwrite" > "$USRFILE"
+ chmod 400 "$USRFILE"
+}
+
+function importTrustStores() {
+ if [ -z "$CONF_DIR" ]; then
+ CONF_DIR="/opt/cryostat.d"
+ fi
+ if [ -z "$SSL_TRUSTSTORE_DIR" ]; then
+ SSL_TRUSTSTORE_DIR="/truststore"
+ fi
+
+ if [ ! -d "$SSL_TRUSTSTORE_DIR" ]; then
+ banner "$SSL_TRUSTSTORE_DIR does not exist; no certificates to import"
+ return 0
+ elif [ ! "$(ls -A "$SSL_TRUSTSTORE_DIR")" ]; then
+ banner "$SSL_TRUSTSTORE_DIR is empty; no certificates to import"
+ return 0
+ fi
+
+ SSL_TRUSTSTORE_PASS="$(cat "${SSL_TRUSTSTORE_PASS_FILE:-$CONF_DIR/truststore.pass}")"
+
+ find "$SSL_TRUSTSTORE_DIR" -type f | while IFS= read -r cert; do
+ echo "Importing certificate $cert ..."
+
+ keytool -importcert -v \
+ -noprompt \
+ -alias "imported-$(basename "$cert")" \
+ -trustcacerts \
+ -keystore "${SSL_TRUSTSTORE:-$CONF_DIR/truststore.p12}" \
+ -file "$cert"\
+ -storepass "$SSL_TRUSTSTORE_PASS"
+ done
+
+ FLAGS+=(
+ "-Djavax.net.ssl.trustStore=$SSL_TRUSTSTORE"
+ "-Djavax.net.ssl.trustStorePassword=$SSL_TRUSTSTORE_PASS"
+ )
+}
+
+FLAGS=()
+importTrustStores
+
+if [ "$CRYOSTAT_DISABLE_JMX_AUTH" = "true" ]; then
+ banner "JMX Auth Disabled"
+ FLAGS+=("-Dcom.sun.management.jmxremote.authenticate=false")
+else
+ createJmxCredentials
+ FLAGS+=("-Dcom.sun.management.jmxremote.authenticate=true")
+ FLAGS+=("-Dcom.sun.management.jmxremote.password.file=$PWFILE")
+ FLAGS+=("-Dcom.sun.management.jmxremote.access.file=$USRFILE")
+fi
+
+export JAVA_OPTS_APPEND="${JAVA_OPTS_APPEND} ${FLAGS[*]}"
+exec $1
diff --git a/src/main/docker/include/genpass.bash b/src/main/docker/include/genpass.bash
new file mode 100755
index 000000000..ba1557267
--- /dev/null
+++ b/src/main/docker/include/genpass.bash
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+genpass() {
+ < /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c32
+}
diff --git a/src/main/docker/include/truststore-setup.bash b/src/main/docker/include/truststore-setup.bash
new file mode 100755
index 000000000..ab4d80cfa
--- /dev/null
+++ b/src/main/docker/include/truststore-setup.bash
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+
+set -e
+
+DIR="$(dirname "$(realpath "$0")")"
+source "${DIR}/genpass.bash"
+
+SSL_TRUSTSTORE_PASS="$(genpass)"
+
+echo "$SSL_TRUSTSTORE_PASS" > "$SSL_TRUSTSTORE_PASS_FILE"
+
+trap "cd -" EXIT
+cd "$CONF_DIR"
+
+keytool -importkeystore \
+ -noprompt \
+ -storetype PKCS12 \
+ -srckeystore /usr/lib/jvm/jre-17-openjdk/lib/security/cacerts \
+ -srcstorepass changeit \
+ -destkeystore "$SSL_TRUSTSTORE" \
+ -deststorepass "$SSL_TRUSTSTORE_PASS"
+
+chmod 664 "${SSL_TRUSTSTORE}"
+chmod 640 "${SSL_TRUSTSTORE_PASS_FILE}"
diff --git a/src/main/java/io/cryostat/ConfigProperties.java b/src/main/java/io/cryostat/ConfigProperties.java
index f69efd3f1..0078a977b 100644
--- a/src/main/java/io/cryostat/ConfigProperties.java
+++ b/src/main/java/io/cryostat/ConfigProperties.java
@@ -51,4 +51,6 @@ public class ConfigProperties {
public static final String STORAGE_TRANSIENT_ARCHIVES_ENABLED =
"storage.transient-archives.enabled";
public static final String STORAGE_TRANSIENT_ARCHIVES_TTL = "storage.transient-archives.ttl";
+
+ public static final String SSL_TRUSTSTORE_DIR = "ssl.truststore.dir";
}
diff --git a/src/main/java/io/cryostat/ExceptionMappers.java b/src/main/java/io/cryostat/ExceptionMappers.java
index acb7c439a..9a05221c7 100644
--- a/src/main/java/io/cryostat/ExceptionMappers.java
+++ b/src/main/java/io/cryostat/ExceptionMappers.java
@@ -19,9 +19,6 @@
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
-import org.openjdk.jmc.rjmx.ConnectionException;
-
-import io.cryostat.targets.TargetConnectionManager;
import io.cryostat.util.EntityExistsException;
import com.nimbusds.jwt.proc.BadJWTException;
@@ -89,22 +86,6 @@ public RestResponse mapIllegalArgumentException(IllegalArgumentException e
return RestResponse.status(HttpResponseStatus.BAD_REQUEST.code());
}
- @ServerExceptionMapper
- public RestResponse mapJmxConnectionException(ConnectionException ex) {
- logger.warn(ex);
- return RestResponse.status(HttpResponseStatus.BAD_GATEWAY.code());
- }
-
- @ServerExceptionMapper
- public RestResponse mapFlightRecorderException(
- org.openjdk.jmc.rjmx.services.jfr.FlightRecorderException ex) {
- logger.warn(ex);
- if (TargetConnectionManager.isJmxAuthFailure(ex)) {
- return RestResponse.status(HttpResponseStatus.FORBIDDEN.code());
- }
- return RestResponse.status(HttpResponseStatus.BAD_GATEWAY.code());
- }
-
@ServerExceptionMapper
public RestResponse mapMutinyTimeoutException(TimeoutException ex) {
logger.warn(ex);
diff --git a/src/main/java/io/cryostat/recordings/RecordingHelper.java b/src/main/java/io/cryostat/recordings/RecordingHelper.java
index 2cb63f8d1..b59a71bb0 100644
--- a/src/main/java/io/cryostat/recordings/RecordingHelper.java
+++ b/src/main/java/io/cryostat/recordings/RecordingHelper.java
@@ -211,9 +211,7 @@ public Uni startRecording(
target,
conn -> {
RecordingOptionsBuilder optionsBuilder =
- recordingOptionsBuilderFactory
- .create(conn.getService())
- .name(options.name());
+ recordingOptionsBuilderFactory.create(target).name(options.name());
if (options.duration().isPresent()) {
optionsBuilder =
optionsBuilder.duration(
@@ -280,8 +278,7 @@ public Uni createSnapshot(Target target) {
String.format("%s-%d", desc.getName().toLowerCase(), desc.getId());
RecordingOptionsBuilder recordingOptionsBuilder =
- recordingOptionsBuilderFactory.create(connection.getService());
- recordingOptionsBuilder.name(rename);
+ recordingOptionsBuilderFactory.create(target);
connection
.getService()
@@ -1146,7 +1143,7 @@ public RecordingNotFoundException(Pair key) {
}
}
- static class SnapshotCreationException extends Exception {
+ public static class SnapshotCreationException extends Exception {
public SnapshotCreationException(String message) {
super(message);
}
diff --git a/src/main/java/io/cryostat/recordings/RecordingOptionsBuilderFactory.java b/src/main/java/io/cryostat/recordings/RecordingOptionsBuilderFactory.java
index 60f7215c9..9b0dde250 100644
--- a/src/main/java/io/cryostat/recordings/RecordingOptionsBuilderFactory.java
+++ b/src/main/java/io/cryostat/recordings/RecordingOptionsBuilderFactory.java
@@ -17,9 +17,27 @@
import org.openjdk.jmc.common.unit.QuantityConversionException;
import org.openjdk.jmc.flightrecorder.configuration.recording.RecordingOptionsBuilder;
-import org.openjdk.jmc.rjmx.services.jfr.IFlightRecorderService;
-public interface RecordingOptionsBuilderFactory {
- RecordingOptionsBuilder create(IFlightRecorderService service)
- throws QuantityConversionException;
+import io.cryostat.targets.Target;
+import io.cryostat.targets.TargetConnectionManager;
+
+import io.smallrye.common.annotation.Blocking;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+@ApplicationScoped
+public class RecordingOptionsBuilderFactory {
+
+ @Inject RecordingOptionsCustomizerFactory customizerFactory;
+ @Inject TargetConnectionManager connectionManager;
+
+ @Blocking
+ public RecordingOptionsBuilder create(Target target) throws QuantityConversionException {
+ return connectionManager.executeConnectedTask(
+ target,
+ conn ->
+ customizerFactory
+ .create(target)
+ .apply(new RecordingOptionsBuilder(conn.getService())));
+ }
}
diff --git a/src/main/java/io/cryostat/recordings/RecordingOptionsCustomizerFactory.java b/src/main/java/io/cryostat/recordings/RecordingOptionsCustomizerFactory.java
new file mode 100644
index 000000000..9177bf18e
--- /dev/null
+++ b/src/main/java/io/cryostat/recordings/RecordingOptionsCustomizerFactory.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright The Cryostat Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.cryostat.recordings;
+
+import java.util.HashMap;
+
+import io.cryostat.core.RecordingOptionsCustomizer;
+import io.cryostat.targets.Target;
+import io.cryostat.targets.Target.EventKind;
+import io.cryostat.targets.Target.TargetDiscovery;
+
+import io.quarkus.vertx.ConsumeEvent;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import org.jboss.logging.Logger;
+
+@ApplicationScoped
+public class RecordingOptionsCustomizerFactory {
+
+ @Inject Logger logger;
+
+ private final HashMap customizers = new HashMap<>();
+
+ @ConsumeEvent(Target.TARGET_JVM_DISCOVERY)
+ void onMessage(TargetDiscovery event) {
+ if (EventKind.LOST.equals(event.kind())) {
+ customizers.remove(event.serviceRef());
+ }
+ }
+
+ public RecordingOptionsCustomizer create(Target target) {
+ return customizers.computeIfAbsent(
+ target, (t) -> new RecordingOptionsCustomizer(logger::debug));
+ }
+}
diff --git a/src/main/java/io/cryostat/recordings/Recordings.java b/src/main/java/io/cryostat/recordings/Recordings.java
index b92865af2..545a72d88 100644
--- a/src/main/java/io/cryostat/recordings/Recordings.java
+++ b/src/main/java/io/cryostat/recordings/Recordings.java
@@ -114,7 +114,7 @@ public class Recordings {
@Inject TargetConnectionManager connectionManager;
@Inject EventBus bus;
@Inject RecordingOptionsBuilderFactory recordingOptionsBuilderFactory;
- @Inject RecordingOptionsCustomizer recordingOptionsCustomizer;
+ @Inject RecordingOptionsCustomizerFactory recordingOptionsCustomizerFactory;
@Inject EventOptionsBuilder.Factory eventOptionsBuilderFactory;
@Inject Clock clock;
@Inject S3Client storage;
@@ -901,8 +901,7 @@ public Map getRecordingOptions(@RestPath long id) throws Excepti
return connectionManager.executeConnectedTask(
target,
connection -> {
- RecordingOptionsBuilder builder =
- recordingOptionsBuilderFactory.create(connection.getService());
+ RecordingOptionsBuilder builder = recordingOptionsBuilderFactory.create(target);
return getRecordingOptions(connection.getService(), builder);
});
}
@@ -923,6 +922,9 @@ public Response patchRecordingOptionsV1(@RestPath URI connectUrl) {
@Blocking
@Path("/api/v3/targets/{id}/recordingOptions")
@RolesAllowed("read")
+ @SuppressFBWarnings(
+ value = "UC_USELESS_OBJECT",
+ justification = "SpotBugs thinks the options map is unused, but it is used")
public Map patchRecordingOptions(
@RestPath long id,
@RestForm String toDisk,
@@ -931,20 +933,20 @@ public Map patchRecordingOptions(
throws Exception {
final String unsetKeyword = "unset";
- Map form = new HashMap<>();
+ Map options = new HashMap<>();
Pattern bool = Pattern.compile("true|false|" + unsetKeyword);
if (toDisk != null) {
Matcher m = bool.matcher(toDisk);
if (!m.matches()) {
throw new BadRequestException("Invalid options");
}
- form.put("toDisk", toDisk);
+ options.put("toDisk", toDisk);
}
if (maxAge != null) {
if (!unsetKeyword.equals(maxAge)) {
try {
Long.parseLong(maxAge);
- form.put("maxAge", maxAge);
+ options.put("maxAge", maxAge);
} catch (NumberFormatException e) {
throw new BadRequestException("Invalid options");
}
@@ -954,31 +956,28 @@ public Map patchRecordingOptions(
if (!unsetKeyword.equals(maxSize)) {
try {
Long.parseLong(maxSize);
- form.put("maxSize", maxSize);
+ options.put("maxSize", maxSize);
} catch (NumberFormatException e) {
throw new BadRequestException("Invalid options");
}
}
}
- form.entrySet()
- .forEach(
- e -> {
- RecordingOptionsCustomizer.OptionKey optionKey =
- RecordingOptionsCustomizer.OptionKey.fromOptionName(e.getKey())
- .get();
- if ("unset".equals(e.getValue())) {
- recordingOptionsCustomizer.unset(optionKey);
- } else {
- recordingOptionsCustomizer.set(optionKey, e.getValue());
- }
- });
-
Target target = Target.find("id", id).singleResult();
+ for (var entry : options.entrySet()) {
+ RecordingOptionsCustomizer.OptionKey optionKey =
+ RecordingOptionsCustomizer.OptionKey.fromOptionName(entry.getKey()).get();
+ var recordingOptionsCustomizer = recordingOptionsCustomizerFactory.create(target);
+ if (unsetKeyword.equals(entry.getValue())) {
+ recordingOptionsCustomizer.unset(optionKey);
+ } else {
+ recordingOptionsCustomizer.set(optionKey, entry.getValue());
+ }
+ }
+
return connectionManager.executeConnectedTask(
target,
connection -> {
- RecordingOptionsBuilder builder =
- recordingOptionsBuilderFactory.create(connection.getService());
+ var builder = recordingOptionsBuilderFactory.create(target);
return getRecordingOptions(connection.getService(), builder);
});
}
diff --git a/src/main/java/io/cryostat/recordings/RecordingsModule.java b/src/main/java/io/cryostat/recordings/RecordingsModule.java
index fa83a5a36..9f04be17c 100644
--- a/src/main/java/io/cryostat/recordings/RecordingsModule.java
+++ b/src/main/java/io/cryostat/recordings/RecordingsModule.java
@@ -15,37 +15,17 @@
*/
package io.cryostat.recordings;
-import org.openjdk.jmc.flightrecorder.configuration.recording.RecordingOptionsBuilder;
-
import io.cryostat.core.EventOptionsBuilder;
-import io.cryostat.core.RecordingOptionsCustomizer;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Singleton;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
@Singleton
public class RecordingsModule {
-
- @Produces
- @ApplicationScoped
- public RecordingOptionsBuilderFactory provideRecordingOptionsBuilderFactory(
- RecordingOptionsCustomizer customizer) {
- return service -> customizer.apply(new RecordingOptionsBuilder(service));
- }
-
@Produces
@ApplicationScoped
public EventOptionsBuilder.Factory provideEventOptionsBuilderFactory() {
return new EventOptionsBuilder.Factory();
}
-
- @Produces
- @ApplicationScoped
- public RecordingOptionsCustomizer provideRecordingOptionsCustomizer() {
- Logger log = LoggerFactory.getLogger(RecordingOptionsCustomizer.class);
- return new RecordingOptionsCustomizer(log::debug);
- }
}
diff --git a/src/main/java/io/cryostat/security/TrustStore.java b/src/main/java/io/cryostat/security/TrustStore.java
new file mode 100644
index 000000000..26f660e40
--- /dev/null
+++ b/src/main/java/io/cryostat/security/TrustStore.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright The Cryostat Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.cryostat.security;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.List;
+
+import io.cryostat.ConfigProperties;
+
+import io.smallrye.common.annotation.Blocking;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.jboss.logging.Logger;
+
+@Path("")
+public class TrustStore {
+
+ @ConfigProperty(name = ConfigProperties.SSL_TRUSTSTORE_DIR)
+ java.nio.file.Path trustStoreDir;
+
+ @Inject Logger logger;
+
+ @Blocking
+ @GET
+ @Path("/api/v3/tls/certs")
+ @Produces(MediaType.APPLICATION_JSON)
+ public List listCerts() throws IOException {
+ return Files.walk(trustStoreDir)
+ .map(java.nio.file.Path::toFile)
+ .filter(File::isFile)
+ .map(File::getPath)
+ .toList();
+ }
+}
diff --git a/src/main/java/io/cryostat/targets/TargetConnectionManager.java b/src/main/java/io/cryostat/targets/TargetConnectionManager.java
index e48e772b1..17b7791f2 100644
--- a/src/main/java/io/cryostat/targets/TargetConnectionManager.java
+++ b/src/main/java/io/cryostat/targets/TargetConnectionManager.java
@@ -31,7 +31,6 @@
import java.util.concurrent.Semaphore;
import javax.management.remote.JMXServiceURL;
-import javax.naming.ServiceUnavailableException;
import javax.security.sasl.SaslException;
import org.openjdk.jmc.rjmx.ConnectionException;
@@ -42,6 +41,7 @@
import io.cryostat.core.net.JFRConnectionToolkit;
import io.cryostat.credentials.Credential;
import io.cryostat.expressions.MatchExpressionEvaluator;
+import io.cryostat.recordings.RecordingHelper.SnapshotCreationException;
import io.cryostat.targets.Target.EventKind;
import io.cryostat.targets.Target.TargetDiscovery;
@@ -54,6 +54,7 @@
import io.quarkus.vertx.ConsumeEvent;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
+import io.vertx.ext.web.handler.HttpException;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
@@ -196,13 +197,24 @@ public Uni executeConnectedTaskUni(Target target, ConnectedTask task)
}
}))
.onFailure(RuntimeException.class)
- .transform(this::unwrapRuntimeException)
+ .transform(t -> unwrapNestedException(RuntimeException.class, t))
.onFailure()
.invoke(logger::warn)
- .onFailure(t -> isTargetConnectionFailure(t) || isUnknownTargetFailure(t))
+ .onFailure(this::isJmxAuthFailure)
+ .transform(t -> new HttpException(427, t))
+ .onFailure(this::isJmxSslFailure)
+ .transform(t -> new HttpException(502, t))
+ .onFailure(this::isServiceTypeFailure)
+ .transform(t -> new HttpException(504, t))
+ .onFailure(
+ t ->
+ !(t instanceof HttpException)
+ && !(t instanceof SnapshotCreationException))
.retry()
.withBackOff(failedBackoff)
- .expireIn(failedTimeout.plusMillis(System.currentTimeMillis()).toMillis());
+ .expireIn(failedTimeout.plusMillis(System.currentTimeMillis()).toMillis())
+ .onFailure(this::isTargetConnectionFailure)
+ .transform(t -> new HttpException(504, t));
}
public T executeConnectedTask(Target target, ConnectedTask task) {
@@ -220,13 +232,24 @@ public Uni executeDirect(
}
}))
.onFailure(RuntimeException.class)
- .transform(this::unwrapRuntimeException)
+ .transform(t -> unwrapNestedException(RuntimeException.class, t))
.onFailure()
.invoke(logger::warn)
- .onFailure(t -> isTargetConnectionFailure(t) || isUnknownTargetFailure(t))
+ .onFailure(this::isJmxAuthFailure)
+ .transform(t -> new HttpException(427, t))
+ .onFailure(this::isJmxSslFailure)
+ .transform(t -> new HttpException(502, t))
+ .onFailure(this::isServiceTypeFailure)
+ .transform(t -> new HttpException(504, t))
+ .onFailure(
+ t ->
+ !(t instanceof HttpException)
+ && !(t instanceof SnapshotCreationException))
.retry()
.withBackOff(failedBackoff)
- .expireIn(failedTimeout.plusMillis(System.currentTimeMillis()).toMillis());
+ .expireIn(failedTimeout.plusMillis(System.currentTimeMillis()).toMillis())
+ .onFailure(this::isTargetConnectionFailure)
+ .transform(t -> new HttpException(504, t));
}
/**
@@ -374,17 +397,21 @@ public interface ConnectedTask {
T execute(JFRConnection connection) throws Exception;
}
- public Throwable unwrapRuntimeException(Throwable t) {
+ public Throwable unwrapNestedException(Class> klazz, Throwable t) {
final int maxDepth = 10;
int depth = 0;
Throwable cause = t;
- while (cause instanceof RuntimeException && depth++ < maxDepth) {
+ while (klazz.isInstance(t) && depth++ < maxDepth) {
+ var c = cause.getCause();
+ if (c == null) {
+ break;
+ }
cause = cause.getCause();
}
return cause;
}
- public static boolean isTargetConnectionFailure(Throwable t) {
+ private boolean isTargetConnectionFailure(Throwable t) {
if (!(t instanceof Exception)) {
return false;
}
@@ -393,16 +420,26 @@ public static boolean isTargetConnectionFailure(Throwable t) {
|| ExceptionUtils.indexOfType(e, FlightRecorderException.class) >= 0;
}
- public static boolean isJmxAuthFailure(Throwable t) {
+ /**
+ * Check if the exception happened because the connection required authentication, and we had no
+ * credentials to present.
+ */
+ private boolean isJmxAuthFailure(Throwable t) {
if (!(t instanceof Exception)) {
return false;
}
Exception e = (Exception) t;
- return ExceptionUtils.indexOfType(e, SecurityException.class) >= 0
+ return ExceptionUtils.indexOfType(e, javax.security.auth.login.FailedLoginException.class)
+ >= 0
+ || ExceptionUtils.indexOfType(e, SecurityException.class) >= 0
|| ExceptionUtils.indexOfType(e, SaslException.class) >= 0;
}
- public static boolean isJmxSslFailure(Throwable t) {
+ /**
+ * Check if the exception happened because the connection presented an SSL/TLS cert which we
+ * don't trust.
+ */
+ private boolean isJmxSslFailure(Throwable t) {
if (!(t instanceof Exception)) {
return false;
}
@@ -412,7 +449,7 @@ public static boolean isJmxSslFailure(Throwable t) {
}
/** Check if the exception happened because the port connected to a non-JMX service. */
- public static boolean isServiceTypeFailure(Throwable t) {
+ private boolean isServiceTypeFailure(Throwable t) {
if (!(t instanceof Exception)) {
return false;
}
@@ -421,23 +458,9 @@ public static boolean isServiceTypeFailure(Throwable t) {
&& ExceptionUtils.indexOfType(e, SocketTimeoutException.class) >= 0;
}
- public static boolean isUnknownTargetFailure(Throwable t) {
- if (!(t instanceof Exception)) {
- return false;
- }
- Exception e = (Exception) t;
- return ExceptionUtils.indexOfType(e, java.net.UnknownHostException.class) >= 0
- || ExceptionUtils.indexOfType(e, java.rmi.UnknownHostException.class) >= 0
- || ExceptionUtils.indexOfType(e, ServiceUnavailableException.class) >= 0;
- }
-
- @Name("io.cryostat.net.TargetConnectionManager.TargetConnectionOpened")
+ @Name("io.cryostat.targets.TargetConnectionManager.TargetConnectionOpened")
@Label("Target Connection Opened")
@Category("Cryostat")
- // @SuppressFBWarnings(
- // value = "URF_UNREAD_FIELD",
- // justification = "The event fields are recorded with JFR instead of accessed
- // directly")
public static class TargetConnectionOpened extends Event {
String serviceUri;
boolean exceptionThrown;
@@ -452,13 +475,9 @@ void setExceptionThrown(boolean exceptionThrown) {
}
}
- @Name("io.cryostat.net.TargetConnectionManager.TargetConnectionClosed")
+ @Name("io.cryostat.targets.TargetConnectionManager.TargetConnectionClosed")
@Label("Target Connection Closed")
@Category("Cryostat")
- // @SuppressFBWarnings(
- // value = "URF_UNREAD_FIELD",
- // justification = "The event fields are recorded with JFR instead of accessed
- // directly")
public static class TargetConnectionClosed extends Event {
URI serviceUri;
boolean exceptionThrown;
diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties
index 08e564fbe..0467ceee7 100644
--- a/src/main/resources/application-dev.properties
+++ b/src/main/resources/application-dev.properties
@@ -6,7 +6,7 @@ quarkus.http.cors=true
# quarkus.http.cors.origins=http://localhost:9000,http://0.0.0.0:9000
quarkus.http.cors.origins=http://localhost:9000
quarkus.http.cors.access-control-allow-credentials=true
-quarkus.http.cors.exposed-headers=X-JMX-Authorization,X-JMX-Authenticate,X-WWW-Authenticate
+quarkus.http.cors.exposed-headers=X-WWW-Authenticate
# quarkus.http.cors.methods=GET,PUT,POST,PATCH,OPTIONS
# quarkus.http.cors.access-control-max-age=1s
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index a8f7ce4fd..d2c9840f3 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -32,6 +32,11 @@ cryostat.http.proxy.host=${quarkus.http.host}
cryostat.http.proxy.port=${quarkus.http.port}
cryostat.http.proxy.path=/
+conf-dir=/opt/cryostat.d
+ssl.truststore=${conf-dir}/truststore.p12
+ssl.truststore.dir=/truststore
+ssl.truststore.pass-file=${conf-dir}/truststore.pass
+
quarkus.http.auth.proactive=false
quarkus.http.host=0.0.0.0
quarkus.http.port=8181
diff --git a/src/main/webui b/src/main/webui
index a9c3d73c4..c93e6b4f7 160000
--- a/src/main/webui
+++ b/src/main/webui
@@ -1 +1 @@
-Subproject commit a9c3d73c421709f5515b71c8742f267b4badc492
+Subproject commit c93e6b4f7a3e7779bb1f9c28a2b7458bb11b1ffa
diff --git a/src/test/java/itest/TargetEventsGetTest.java b/src/test/java/itest/TargetEventsGetTest.java
index 0323d8d5a..aa0e6582d 100644
--- a/src/test/java/itest/TargetEventsGetTest.java
+++ b/src/test/java/itest/TargetEventsGetTest.java
@@ -114,7 +114,7 @@ public void testGetTargetEventsV2WithQueryReturnsRequestedEvents() throws Except
LinkedHashMap expectedResults = new LinkedHashMap();
expectedResults.put("name", "Target Connection Opened");
expectedResults.put(
- "typeId", "io.cryostat.net.TargetConnectionManager.TargetConnectionOpened");
+ "typeId", "io.cryostat.targets.TargetConnectionManager.TargetConnectionOpened");
expectedResults.put("description", "");
expectedResults.put("category", List.of("Cryostat"));
expectedResults.put(