diff --git a/java-spiffe-core/README.md b/java-spiffe-core/README.md
index f8a8394..36d3c14 100644
--- a/java-spiffe-core/README.md
+++ b/java-spiffe-core/README.md
@@ -6,4 +6,10 @@ Core functionality to fetch X509 and JWT SVIDs from the Workload API.
```
TBD
-```
\ No newline at end of file
+```
+
+## Netty Event Loop thread number configuration
+
+Use the variable `io.netty.eventLoopThreads` to configure the number of threads for the Netty Event Loop Group.
+
+By default, it is `availableProcessors * 2`.
diff --git a/java-spiffe-core/build.gradle b/java-spiffe-core/build.gradle
index e2d3c71..5158633 100644
--- a/java-spiffe-core/build.gradle
+++ b/java-spiffe-core/build.gradle
@@ -13,8 +13,8 @@ buildscript {
}
ext {
- grpcVersion = '1.28.0'
- nettyVersion = '4.1.33.Final'
+ grpcVersion = '1.29.0'
+ nettyVersion = '4.1.49.Final'
protobufProtocVersion = '3.11.4'
}
@@ -49,6 +49,7 @@ dependencies {
implementation group: 'io.grpc', name: 'grpc-netty', version: "${grpcVersion}"
implementation group: 'io.grpc', name: 'grpc-protobuf', version: "${grpcVersion}"
implementation group: 'io.grpc', name: 'grpc-stub', version: "${grpcVersion}"
+
implementation group: 'io.netty', name: 'netty-transport-native-epoll', version: "${nettyVersion}", classifier: 'linux-x86_64'
implementation group: 'io.netty', name: 'netty-transport-native-kqueue', version: "${nettyVersion}", classifier: 'osx-x86_64'
compileOnly group: 'javax.annotation', name: 'javax.annotation-api', version: '1.3.2'
diff --git a/java-spiffe-core/src/main/java/spiffe/exception/X509ContextException.java b/java-spiffe-core/src/main/java/spiffe/exception/X509ContextException.java
index c01d232..72046ba 100644
--- a/java-spiffe-core/src/main/java/spiffe/exception/X509ContextException.java
+++ b/java-spiffe-core/src/main/java/spiffe/exception/X509ContextException.java
@@ -1,10 +1,10 @@
package spiffe.exception;
/**
- * Unchecked exception thrown when a there was an error retrieving
+ * Checked exception thrown when a there was an error retrieving
* or processing a X509Context.
*/
-public class X509ContextException extends RuntimeException {
+public class X509ContextException extends Exception {
public X509ContextException(String message) {
super(message);
}
diff --git a/java-spiffe-core/src/main/java/spiffe/exception/X509SourceException.java b/java-spiffe-core/src/main/java/spiffe/exception/X509SourceException.java
index 090dc81..5f93eb1 100644
--- a/java-spiffe-core/src/main/java/spiffe/exception/X509SourceException.java
+++ b/java-spiffe-core/src/main/java/spiffe/exception/X509SourceException.java
@@ -1,9 +1,9 @@
package spiffe.exception;
/**
- * Unchecked thrown when there is an error creating or initializing a X.509 source
+ * Checked thrown when there is an error creating or initializing a X.509 source
*/
-public class X509SourceException extends RuntimeException {
+public class X509SourceException extends Exception {
public X509SourceException(String message) {
super(message);
}
diff --git a/java-spiffe-core/src/main/java/spiffe/spiffeid/SpiffeId.java b/java-spiffe-core/src/main/java/spiffe/spiffeid/SpiffeId.java
index e28fd1e..2101d82 100644
--- a/java-spiffe-core/src/main/java/spiffe/spiffeid/SpiffeId.java
+++ b/java-spiffe-core/src/main/java/spiffe/spiffeid/SpiffeId.java
@@ -17,7 +17,7 @@ import java.util.stream.Collectors;
@Value
public class SpiffeId {
- private static final String SPIFFE_SCHEMA = "spiffe";
+ public static final String SPIFFE_SCHEME = "spiffe";
TrustDomain trustDomain;
@@ -60,7 +60,7 @@ public class SpiffeId {
val uri = URI.create(spiffeIdAsString);
- if (!SPIFFE_SCHEMA.equals(uri.getScheme())) {
+ if (!SPIFFE_SCHEME.equals(uri.getScheme())) {
throw new IllegalArgumentException("Invalid SPIFFE schema");
}
@@ -81,7 +81,7 @@ public class SpiffeId {
@Override
public String toString() {
- return String.format("%s://%s%s", SPIFFE_SCHEMA, this.trustDomain.toString(), this.path);
+ return String.format("%s://%s%s", SPIFFE_SCHEME, this.trustDomain.toString(), this.path);
}
private static String normalize(String s) {
diff --git a/java-spiffe-core/src/main/java/spiffe/spiffeid/TrustDomain.java b/java-spiffe-core/src/main/java/spiffe/spiffeid/TrustDomain.java
index 4e22537..6fb8f4e 100644
--- a/java-spiffe-core/src/main/java/spiffe/spiffeid/TrustDomain.java
+++ b/java-spiffe-core/src/main/java/spiffe/spiffeid/TrustDomain.java
@@ -9,7 +9,7 @@ import org.apache.commons.lang3.StringUtils;
import java.net.URI;
import java.net.URISyntaxException;
-import static java.lang.String.format;
+import static spiffe.spiffeid.SpiffeId.SPIFFE_SCHEME;
/**
* A TrustDomain represents a normalized SPIFFE trust domain (e.g. domain.test).
@@ -33,15 +33,47 @@ public class TrustDomain {
*/
public static TrustDomain of(@NonNull String trustDomain) {
if (StringUtils.isBlank(trustDomain)) {
- throw new IllegalArgumentException("Trust Domain cannot be empty");
+ throw new IllegalArgumentException("Trust domain cannot be empty");
}
+
+ URI uri;
try {
- val uri = new URI(normalize(trustDomain));
- val host = getHost(uri);
- return new TrustDomain(host);
+ val normalized = normalize(trustDomain);
+ uri = new URI(normalized);
+ validateUri(uri);
} catch (URISyntaxException e) {
- throw new IllegalArgumentException(format("Unable to parse: %s", trustDomain), e);
+ throw new IllegalArgumentException(e.getMessage());
}
+
+ val host = uri.getHost();
+ validateHost(host);
+ return new TrustDomain(host);
+ }
+
+ private static void validateHost(String host) {
+ if (StringUtils.isBlank(host)) {
+ throw new IllegalArgumentException("Trust domain cannot be empty");
+ }
+ }
+
+ private static void validateUri(URI uri) {
+ val scheme = uri.getScheme();
+ if (StringUtils.isNotBlank(scheme) && !SPIFFE_SCHEME.equals(scheme)) {
+ throw new IllegalArgumentException("Invalid scheme");
+ }
+
+ val port = uri.getPort();
+ if (port != -1) {
+ throw new IllegalArgumentException("Port is not allowed");
+ }
+ }
+
+ private static String normalize(String s) {
+ s = s.toLowerCase().trim();
+ if (!s.contains("://")) {
+ s = SPIFFE_SCHEME.concat("://").concat(s);
+ }
+ return s;
}
/**
@@ -53,15 +85,4 @@ public class TrustDomain {
public String toString() {
return name;
}
-
- private static String normalize(String s) {
- return s.toLowerCase().trim();
- }
-
- private static String getHost(URI uri) {
- if (StringUtils.isBlank(uri.getHost())) {
- return uri.getPath();
- }
- return uri.getHost();
- }
}
diff --git a/java-spiffe-core/src/main/java/spiffe/workloadapi/WorkloadApiClient.java b/java-spiffe-core/src/main/java/spiffe/workloadapi/WorkloadApiClient.java
index def9cc0..b0347ad 100644
--- a/java-spiffe-core/src/main/java/spiffe/workloadapi/WorkloadApiClient.java
+++ b/java-spiffe-core/src/main/java/spiffe/workloadapi/WorkloadApiClient.java
@@ -2,6 +2,7 @@ package spiffe.workloadapi;
import io.grpc.Context;
import io.grpc.ManagedChannel;
+import io.grpc.Status;
import io.grpc.stub.StreamObserver;
import lombok.Builder;
import lombok.Data;
@@ -22,6 +23,8 @@ import spiffe.workloadapi.internal.SecurityHeaderInterceptor;
import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc;
import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIBlockingStub;
import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIStub;
+import spiffe.workloadapi.retry.BackoffPolicy;
+import spiffe.workloadapi.retry.RetryHandler;
import java.io.Closeable;
import java.security.cert.CertificateException;
@@ -29,34 +32,55 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.logging.Level;
import static spiffe.workloadapi.internal.Workload.X509SVIDRequest;
import static spiffe.workloadapi.internal.Workload.X509SVIDResponse;
/**
* A WorkloadApiClient represents a client to interact with the Workload API.
- * Supports one-shot calls and watch updates for X.509 and JWT SVIDS and bundles.
+ *
+ * Supports one-shot calls and watch updates for X.509 and JWT SVIDs and bundles.
+ *
+ * The watch for updates methods support retries using an exponential backoff policy to reestablish
+ * the stream connection to the Workload API.
*/
@Log
public class WorkloadApiClient implements Closeable {
+ private static final String INVALID_ARGUMENT = "INVALID_ARGUMENT";
+
private final SpiffeWorkloadAPIStub workloadApiAsyncStub;
private final SpiffeWorkloadAPIBlockingStub workloadApiBlockingStub;
private final ManagedChannel managedChannel;
private final List cancellableContexts;
+ private final BackoffPolicy backoffPolicy;
- private WorkloadApiClient(SpiffeWorkloadAPIStub workloadApiAsyncStub, SpiffeWorkloadAPIBlockingStub workloadApiBlockingStub, ManagedChannel managedChannel) {
+ // using a scheduled executor service to be able to schedule retries
+ // it is injected in each of the retryHandlers in the watch methods
+ private final ScheduledExecutorService retryExecutor;
+
+ private boolean closed;
+
+ private WorkloadApiClient(SpiffeWorkloadAPIStub workloadApiAsyncStub,
+ SpiffeWorkloadAPIBlockingStub workloadApiBlockingStub,
+ ManagedChannel managedChannel,
+ BackoffPolicy backoffPolicy, ScheduledExecutorService retryExecutor) {
this.workloadApiAsyncStub = workloadApiAsyncStub;
this.workloadApiBlockingStub = workloadApiBlockingStub;
this.managedChannel = managedChannel;
this.cancellableContexts = Collections.synchronizedList(new ArrayList<>());
+ this.backoffPolicy = backoffPolicy;
+ this.retryExecutor = retryExecutor;
}
/**
* Creates a new Workload API client using the default socket endpoint address.
- * @see Address#getDefaultAddress()
*
* @return a {@link WorkloadApiClient}
+ * @see Address#getDefaultAddress()
*/
public static WorkloadApiClient newClient() throws SocketEndpointAddressException {
val options = ClientOptions.builder().build();
@@ -79,9 +103,12 @@ public class WorkloadApiClient implements Closeable {
spiffeSocketPath = Address.getDefaultAddress();
}
+ if (options.backoffPolicy == null) {
+ options.backoffPolicy = new BackoffPolicy();
+ }
+
val socketEndpointAddress = Address.parseAddress(spiffeSocketPath);
val managedChannel = GrpcManagedChannelFactory.newChannel(socketEndpointAddress);
-
val workloadAPIAsyncStub = SpiffeWorkloadAPIGrpc
.newStub(managedChannel)
.withInterceptors(new SecurityHeaderInterceptor());
@@ -90,7 +117,14 @@ public class WorkloadApiClient implements Closeable {
.newBlockingStub(managedChannel)
.withInterceptors(new SecurityHeaderInterceptor());
- return new WorkloadApiClient(workloadAPIAsyncStub, workloadAPIBlockingStub, managedChannel);
+ val retryExecutor = Executors.newSingleThreadScheduledExecutor();
+
+ return new WorkloadApiClient(
+ workloadAPIAsyncStub,
+ workloadAPIBlockingStub,
+ managedChannel,
+ options.backoffPolicy,
+ retryExecutor);
}
/**
@@ -98,18 +132,12 @@ public class WorkloadApiClient implements Closeable {
*
* @throws X509ContextException if there is an error fetching or processing the X.509 context
*/
- public X509Context fetchX509Context() {
- Context.CancellableContext cancellableContext;
- cancellableContext = Context.current().withCancellation();
- X509Context result;
- try {
- result = cancellableContext.call(this::processX509Context);
+ public X509Context fetchX509Context() throws X509ContextException {
+ try (val cancellableContext = Context.current().withCancellation()) {
+ return cancellableContext.call(this::processX509Context);
} catch (Exception e) {
throw new X509ContextException("Error fetching X509Context", e);
}
- // close connection
- cancellableContext.close();
- return result;
}
/**
@@ -118,32 +146,49 @@ public class WorkloadApiClient implements Closeable {
* @param watcher an instance that implements a {@link Watcher}.
*/
public void watchX509Context(Watcher watcher) {
- StreamObserver streamObserver = new StreamObserver() {
+ val retryHandler = new RetryHandler(backoffPolicy, retryExecutor);
+ val cancellableContext = Context.current().withCancellation();
+
+ val streamObserver = getX509ContextStreamObserver(watcher, retryHandler, cancellableContext);
+
+ cancellableContext.run(() -> workloadApiAsyncStub.fetchX509SVID(newX509SvidRequest(), streamObserver));
+ this.cancellableContexts.add(cancellableContext);
+ }
+
+ private StreamObserver getX509ContextStreamObserver(Watcher watcher, RetryHandler retryHandler, Context.CancellableContext cancellableContext) {
+ return new StreamObserver() {
@Override
public void onNext(X509SVIDResponse value) {
- X509Context x509Context = null;
try {
- x509Context = GrpcConversionUtils.toX509Context(value);
+ X509Context x509Context = GrpcConversionUtils.toX509Context(value);
+ watcher.onUpdate(x509Context);
+ retryHandler.reset();
} catch (CertificateException | X509SvidException e) {
watcher.onError(new X509ContextException("Error processing X509 Context update", e));
}
- watcher.onUpdate(x509Context);
}
@Override
public void onError(Throwable t) {
- watcher.onError(new X509ContextException("Error getting X509Context", t));
+ handleWatchX509ContextError(t);
+ }
+
+ private void handleWatchX509ContextError(Throwable t) {
+ if (INVALID_ARGUMENT.equals(Status.fromThrowable(t).getCode().name())) {
+ watcher.onError(new X509ContextException("Canceling X509 Context watch", t));
+ } else {
+ log.log(Level.INFO, "Retrying connecting to Workload API to register X509 context watcher");
+ retryHandler.scheduleRetry(() ->
+ cancellableContext.run(() -> workloadApiAsyncStub.fetchX509SVID(newX509SvidRequest(), this)));
+ }
}
@Override
public void onCompleted() {
+ cancellableContext.close();
watcher.onError(new X509ContextException("Unexpected completed stream"));
}
};
- Context.CancellableContext cancellableContext;
- cancellableContext = Context.current().withCancellation();
- cancellableContext.run(() -> workloadApiAsyncStub.fetchX509SVID(newX509SvidRequest(), streamObserver));
- this.cancellableContexts.add(cancellableContext);
}
/**
@@ -152,9 +197,7 @@ public class WorkloadApiClient implements Closeable {
* @param subject a SPIFFE ID
* @param audience the audience of the JWT-SVID
* @param extraAudience the extra audience for the JWT_SVID
- *
* @return an instance of a {@link JwtSvid}
- *
* @throws //TODO: declare thrown exceptions
*/
public JwtSvid fetchJwtSvid(SpiffeId subject, String audience, String... extraAudience) {
@@ -178,7 +221,6 @@ public class WorkloadApiClient implements Closeable {
* @param token JWT token
* @param audience audience of the JWT
* @return the {@link JwtSvid} if the token and audience could be validated.
- *
* @throws //TODO: declare thrown exceptions
*/
public JwtSvid validateJwtSvid(String token, String audience) {
@@ -199,42 +241,49 @@ public class WorkloadApiClient implements Closeable {
*/
@Override
public void close() {
- log.info("Closing WorkloadAPI client");
+ log.log(Level.FINE, "Closing WorkloadAPI client");
synchronized (this) {
- for (val context : cancellableContexts) {
- context.close();
+ if (!closed) {
+ closed = true;
+ for (val context : cancellableContexts) {
+ context.close();
+ }
+ managedChannel.shutdown();
+ retryExecutor.shutdown();
}
- log.info("Shutting down Managed Channel");
- managedChannel.shutdown();
}
+ log.log(Level.FINE, "WorkloadAPI client is closed");
}
private X509SVIDRequest newX509SvidRequest() {
return X509SVIDRequest.newBuilder().build();
}
- private X509Context processX509Context() {
+ private X509Context processX509Context() throws X509ContextException {
try {
Iterator x509SVIDResponse = workloadApiBlockingStub.fetchX509SVID(newX509SvidRequest());
if (x509SVIDResponse.hasNext()) {
return GrpcConversionUtils.toX509Context(x509SVIDResponse.next());
}
- } catch (Exception e) {
+ } catch (CertificateException | X509SvidException e) {
throw new X509ContextException("Error processing X509Context", e);
}
throw new X509ContextException("Error processing X509Context: x509SVIDResponse is empty");
}
/**
- * Options for creating a new {@link WorkloadApiClient}.
+ * Options for creating a new {@link WorkloadApiClient}. The {@link BackoffPolicy} is used
+ * to configure a {@link RetryHandler} to perform retries to reconnect to the Workload API.
*/
@Data
public static class ClientOptions {
String spiffeSocketPath;
+ BackoffPolicy backoffPolicy;
@Builder
- public ClientOptions(String spiffeSocketPath) {
+ public ClientOptions(String spiffeSocketPath, BackoffPolicy backoffPolicy) {
this.spiffeSocketPath = spiffeSocketPath;
+ this.backoffPolicy = backoffPolicy;
}
}
}
diff --git a/java-spiffe-core/src/main/java/spiffe/workloadapi/X509Source.java b/java-spiffe-core/src/main/java/spiffe/workloadapi/X509Source.java
index 9564c86..ae519cf 100644
--- a/java-spiffe-core/src/main/java/spiffe/workloadapi/X509Source.java
+++ b/java-spiffe-core/src/main/java/spiffe/workloadapi/X509Source.java
@@ -11,6 +11,7 @@ import spiffe.bundle.x509bundle.X509BundleSet;
import spiffe.bundle.x509bundle.X509BundleSource;
import spiffe.exception.BundleNotFoundException;
import spiffe.exception.SocketEndpointAddressException;
+import spiffe.exception.X509ContextException;
import spiffe.exception.X509SourceException;
import spiffe.spiffeid.TrustDomain;
import spiffe.svid.x509svid.X509Svid;
@@ -57,7 +58,7 @@ public class X509Source implements X509SvidSource, X509BundleSource, Closeable {
* @throws SocketEndpointAddressException if the address to the Workload API is not valid
* @throws X509SourceException if the source could not be initialized
*/
- public static X509Source newSource() throws SocketEndpointAddressException {
+ public static X509Source newSource() throws SocketEndpointAddressException, X509SourceException {
X509SourceOptions x509SourceOptions = X509SourceOptions.builder().build();
return newSource(x509SourceOptions);
}
@@ -75,7 +76,7 @@ public class X509Source implements X509SvidSource, X509BundleSource, Closeable {
* @throws SocketEndpointAddressException if the address to the Workload API is not valid
* @throws X509SourceException if the source could not be initialized
*/
- public static X509Source newSource(@NonNull X509SourceOptions options) throws SocketEndpointAddressException {
+ public static X509Source newSource(@NonNull X509SourceOptions options) throws SocketEndpointAddressException, X509SourceException {
if (options.workloadApiClient == null) {
options.workloadApiClient = createClient(options);
}
@@ -149,7 +150,7 @@ public class X509Source implements X509SvidSource, X509BundleSource, Closeable {
return WorkloadApiClient.newClient(clientOptions);
}
- private void init() {
+ private void init() throws X509ContextException {
X509Context x509Context = workloadApiClient.fetchX509Context();
setX509Context(x509Context);
setX509ContextWatcher();
diff --git a/java-spiffe-core/src/main/java/spiffe/workloadapi/retry/BackoffPolicy.java b/java-spiffe-core/src/main/java/spiffe/workloadapi/retry/BackoffPolicy.java
new file mode 100644
index 0000000..9258663
--- /dev/null
+++ b/java-spiffe-core/src/main/java/spiffe/workloadapi/retry/BackoffPolicy.java
@@ -0,0 +1,79 @@
+package spiffe.workloadapi.retry;
+
+import lombok.Builder;
+import lombok.Data;
+import lombok.val;
+
+import java.time.Duration;
+import java.util.function.UnaryOperator;
+
+@Data
+public class BackoffPolicy {
+
+ /**
+ * Retry indefinitely, default behavior
+ */
+ public static final int UNLIMITED_RETRIES = -1;
+
+ private static final int BACKOFF_MULTIPLIER = 2;
+
+ /**
+ * The first backoff delay period
+ */
+ Duration initialDelay = Duration.ofSeconds(1);
+
+ /**
+ * Max time of delay for the backoff period
+ */
+ Duration maxDelay = Duration.ofSeconds(60);
+
+ /**
+ * Max number of retries, unlimited by default
+ */
+ int maxRetries = UNLIMITED_RETRIES;
+
+ /**
+ * Function to calculate the backoff delay
+ */
+ UnaryOperator backoffFunction = d -> d.multipliedBy(BACKOFF_MULTIPLIER);
+
+ /**
+ * Build backoff policy with defaults
+ */
+ public BackoffPolicy() {
+ }
+
+ @Builder
+ public BackoffPolicy(Duration initialDelay, Duration maxDelay, int maxRetries, UnaryOperator backoffFunction) {
+ this.initialDelay = initialDelay;
+ this.maxDelay = maxDelay;
+ this.maxRetries = maxRetries;
+ this.backoffFunction = backoffFunction;
+ }
+
+ /**
+ * Calculate the nextDelay based on a currentDelay, applying the backoff function
+ * If the calculated delay is greater than maxDelay, it returns maxDelay
+ *
+ * @param currentDelay
+ * @return next delay
+ */
+ public Duration nextDelay(Duration currentDelay) {
+ val next = backoffFunction.apply(currentDelay);
+ if (next.compareTo(maxDelay) > 0) {
+ return maxDelay;
+ }
+ return next;
+ }
+
+ /**
+ * Returns true if the RetryPolicy is configure with UNLIMITED_RETRIES
+ * or if the retriesCount param is lower than the maxRetries
+ *
+ * @param retriesCount the current number of retries
+ * @return
+ */
+ public boolean doNotExceedMaxRetries(int retriesCount) {
+ return (maxRetries == UNLIMITED_RETRIES || retriesCount < maxRetries);
+ }
+}
diff --git a/java-spiffe-core/src/main/java/spiffe/workloadapi/retry/RetryHandler.java b/java-spiffe-core/src/main/java/spiffe/workloadapi/retry/RetryHandler.java
new file mode 100644
index 0000000..ba83f79
--- /dev/null
+++ b/java-spiffe-core/src/main/java/spiffe/workloadapi/retry/RetryHandler.java
@@ -0,0 +1,42 @@
+package spiffe.workloadapi.retry;
+
+import java.time.Duration;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Provides methods to schedule the execution of retries based on a backoff policy
+ */
+public class RetryHandler {
+
+ public final ScheduledExecutorService executor;
+ private final BackoffPolicy backoffPolicy;
+ private Duration nextDelay;
+ private int retryCount;
+
+ public RetryHandler(BackoffPolicy backoffPolicy, ScheduledExecutorService executor) {
+ this.nextDelay = backoffPolicy.getInitialDelay();
+ this.backoffPolicy = backoffPolicy;
+ this.executor = executor;
+ }
+
+ /**
+ * Schedule to execute a Runnable, based on the backoff policy
+ * Updates the next delay and retries count
+ */
+ public void scheduleRetry(Runnable runnable) {
+ if (backoffPolicy.doNotExceedMaxRetries(retryCount)) {
+ executor.schedule(runnable, nextDelay.getSeconds(), TimeUnit.SECONDS);
+ nextDelay = backoffPolicy.nextDelay(nextDelay);
+ retryCount++;
+ }
+ }
+
+ /**
+ * Reset state of RetryHandle to initial values
+ */
+ public void reset() {
+ nextDelay = backoffPolicy.getInitialDelay();
+ retryCount = 0;
+ }
+}
diff --git a/java-spiffe-core/src/test/java/spiffe/spiffeid/TrustDomainTest.java b/java-spiffe-core/src/test/java/spiffe/spiffeid/TrustDomainTest.java
index f18a421..79483e7 100644
--- a/java-spiffe-core/src/test/java/spiffe/spiffeid/TrustDomainTest.java
+++ b/java-spiffe-core/src/test/java/spiffe/spiffeid/TrustDomainTest.java
@@ -1,86 +1,43 @@
package spiffe.spiffeid;
-import lombok.val;
-import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
public class TrustDomainTest {
-
- @Test
- void of_givenAString_returnTrustDomain() {
- val trustDomain = TrustDomain.of("domain.test");
- assertEquals("domain.test", trustDomain.toString());
+
+ static Stream provideTestTrustDomain() {
+ return Stream.of(
+ Arguments.of("", "Trust domain cannot be empty"),
+ Arguments.of(null, "trustDomain is marked non-null but is null"),
+ Arguments.of(" DomAin.TesT ", "domain.test"),
+ Arguments.of(" spiffe://domaiN.Test ", "domain.test"),
+ Arguments.of("spiffe://domain.test/path/element", "domain.test"),
+ Arguments.of("spiffe://domain.test/spiffe://domain.test/path/element", "domain.test"),
+ Arguments.of("spiffe://domain.test/spiffe://domain.test:80/path/element", "domain.test"),
+ Arguments.of("http://domain.test", "Invalid scheme"),
+ Arguments.of("spiffe:// domain.test ", "Illegal character in authority at index 9: spiffe:// domain.test"),
+ Arguments.of("://domain.test", "Expected scheme name at index 0: ://domain.test"),
+ Arguments.of("spiffe:///path/element", "Trust domain cannot be empty"),
+ Arguments.of("/path/element", "Trust domain cannot be empty"),
+ Arguments.of("spiffe://domain.test:80", "Port is not allowed")
+ );
}
- @Test
- void of_givenASpiffeIdString_returnTrustDomainWithHostPart() {
- val trustDomain = TrustDomain.of("spiffe://domain.test");
- assertEquals("domain.test", trustDomain.toString());
- }
-
- @Test
- void of_givenASpiffeIdStringWithPath_returnTrustDomainWithHostPart() {
- val trustDomain = TrustDomain.of("spiffe://domain.test/workload");
- assertEquals("domain.test", trustDomain.toString());
- }
-
- @Test
- void of_givenAStringWithCaps_returnNormalizedTrustDomain() {
- val trustDomain = TrustDomain.of("DoMAin.TesT");
-
- assertEquals("domain.test", trustDomain.toString());
- }
-
- @Test
- void of_givenAStringWithTrailingAndLeadingBlanks_returnNormalizedTrustDomain() {
- val trustDomain = TrustDomain.of(" domain.test ");
-
- assertEquals("domain.test", trustDomain.toString());
- }
-
- @Test
- void of_nullString_ThrowsIllegalArgumentException() {
+ @ParameterizedTest
+ @MethodSource("provideTestTrustDomain")
+ void parseAddressInvalid(String input, Object expected) {
+ TrustDomain result = null;
try {
- TrustDomain.of(null);
- } catch (NullPointerException e) {
- assertEquals("trustDomain is marked non-null but is null", e.getMessage());
+ result = TrustDomain.of(input);
+ assertEquals(expected, result.getName());
+ } catch (Exception e) {
+ assertEquals(expected, e.getMessage());
}
}
-
- @Test
- void of_emptyString_ThrowsIllegalArgumentException() {
- try {
- TrustDomain.of("");
- } catch (IllegalArgumentException e) {
- assertEquals("Trust Domain cannot be empty", e.getMessage());
- }
- }
-
- @Test
- void of_blankString_ThrowsIllegalArgumentException() {
- try {
- TrustDomain.of(" ");
- } catch (IllegalArgumentException e) {
- assertEquals("Trust Domain cannot be empty", e.getMessage());
- }
- }
-
- @Test
- void equals_twoTrustDomainObjectsWithTheSameString_returnsTrue() {
- val trustDomain1 = TrustDomain.of("example.org");
- val trustDomain2 = TrustDomain.of("example.org");
-
- assertEquals(trustDomain1, trustDomain2);
- }
-
- @Test
- void equals_twoTrustDomainObjectsWithDifferentStrings_returnsFalse() {
- val trustDomain1 = TrustDomain.of("example.org");
- val trustDomain2 = TrustDomain.of("other.org");
-
- assertNotEquals(trustDomain1, trustDomain2);
- }
}
diff --git a/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeKeyManagerFactory.java b/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeKeyManagerFactory.java
index 7a1162f..43d581e 100644
--- a/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeKeyManagerFactory.java
+++ b/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeKeyManagerFactory.java
@@ -1,6 +1,8 @@
package spiffe.provider;
import lombok.val;
+import spiffe.exception.SocketEndpointAddressException;
+import spiffe.exception.X509SourceException;
import spiffe.svid.x509svid.X509SvidSource;
import javax.net.ssl.KeyManager;
@@ -25,10 +27,19 @@ public final class SpiffeKeyManagerFactory extends KeyManagerFactorySpi {
/**
* Default method for creating the KeyManager, uses a X509Source instance
* that is handled by the Singleton {@link X509SourceManager}
+ *
+ * @throws SpiffeProviderException in case there is an error setting up the X509 source
*/
@Override
protected KeyManager[] engineGetKeyManagers() {
- val spiffeKeyManager = new SpiffeKeyManager(X509SourceManager.INSTANCE.getX509Source());
+ SpiffeKeyManager spiffeKeyManager;
+ try {
+ spiffeKeyManager = new SpiffeKeyManager(X509SourceManager.getX509Source());
+ } catch (X509SourceException e) {
+ throw new SpiffeProviderException("The X509 source could not be created", e);
+ } catch (SocketEndpointAddressException e) {
+ throw new SpiffeProviderException("The Workload API Socket endpoint address configured is not valid", e);
+ }
return new KeyManager[]{spiffeKeyManager};
}
diff --git a/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeProviderConstants.java b/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeProviderConstants.java
index 7115202..63d15ed 100644
--- a/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeProviderConstants.java
+++ b/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeProviderConstants.java
@@ -5,6 +5,12 @@ package spiffe.provider;
*/
class SpiffeProviderConstants {
+ /**
+ * Security property to get the list of accepted SPIFFE IDs.
+ * This property is read in the java.security file
+ */
+ static final String SSL_SPIFFE_ACCEPT_PROPERTY = "ssl.spiffe.accept";
+
// the name of this Provider implementation
static final String PROVIDER_NAME = "Spiffe";
diff --git a/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeProviderException.java b/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeProviderException.java
new file mode 100644
index 0000000..d84853a
--- /dev/null
+++ b/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeProviderException.java
@@ -0,0 +1,20 @@
+package spiffe.provider;
+
+/**
+ * Unchecked exception thrown when there is an error setting up the
+ * source of svids and bundles.
+ */
+public class SpiffeProviderException extends RuntimeException {
+
+ public SpiffeProviderException(String message) {
+ super(message);
+ }
+
+ public SpiffeProviderException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public SpiffeProviderException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeSslContextFactory.java b/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeSslContextFactory.java
index e8ddc8a..af7d5f3 100644
--- a/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeSslContextFactory.java
+++ b/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeSslContextFactory.java
@@ -23,7 +23,7 @@ public final class SpiffeSslContextFactory {
private static final String DEFAULT_SSL_PROTOCOL = "TLSv1.2";
/**
- * Creates an SSLContext initialized with a SPIFFE KeyManager and TrustManager that are backed by
+ * Creates an {@link SSLContext} initialized with a SPIFFE KeyManager and TrustManager that are backed by
* the Workload API via a X509Source.
*
* @param options {@link SslContextOptions}. The option {@link X509Source} must be not null.
diff --git a/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeTrustManagerFactory.java b/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeTrustManagerFactory.java
index 871ff78..8440ea6 100644
--- a/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeTrustManagerFactory.java
+++ b/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeTrustManagerFactory.java
@@ -2,6 +2,8 @@ package spiffe.provider;
import lombok.val;
import spiffe.bundle.x509bundle.X509BundleSource;
+import spiffe.exception.SocketEndpointAddressException;
+import spiffe.exception.X509SourceException;
import spiffe.spiffeid.SpiffeId;
import spiffe.spiffeid.SpiffeIdUtils;
@@ -12,6 +14,8 @@ import java.security.KeyStore;
import java.util.List;
import java.util.function.Supplier;
+import static spiffe.provider.SpiffeProviderConstants.SSL_SPIFFE_ACCEPT_PROPERTY;
+
/**
* A SpiffeTrustManagerFactory is an implementation of a {@link javax.net.ssl.TrustManagerFactory} to create a
* TrustManager backed by a X509BundleSource that is maintained via the Workload API.
@@ -27,9 +31,6 @@ import java.util.function.Supplier;
*/
public class SpiffeTrustManagerFactory extends TrustManagerFactorySpi {
- // System property to get the list of accepted SPIFFE IDs
- private static final String SSL_SPIFFE_ACCEPT_PROPERTY = "ssl.spiffe.accept";
-
/**
* Default method for creating a TrustManager initializing it with
* the {@link spiffe.workloadapi.X509Source} instance
@@ -38,14 +39,22 @@ public class SpiffeTrustManagerFactory extends TrustManagerFactorySpi {
* from the System Property defined in SSL_SPIFFE_ACCEPT_PROPERTY.
*
* @return a TrustManager array with an initialized TrustManager.
+ * @throws SpiffeProviderException in case there is an error setting up the X509 source
*/
@Override
public TrustManager[] engineGetTrustManagers() {
- val spiffeTrustManager =
- new SpiffeTrustManager(
- X509SourceManager.INSTANCE.getX509Source(),
- this::getAcceptedSpiffeIds
- );
+ SpiffeTrustManager spiffeTrustManager =
+ null;
+ try {
+ spiffeTrustManager = new SpiffeTrustManager(
+ X509SourceManager.getX509Source(),
+ this::getAcceptedSpiffeIds
+ );
+ } catch (X509SourceException e) {
+ throw new SpiffeProviderException("The X509 source could not be created", e);
+ } catch (SocketEndpointAddressException e) {
+ throw new SpiffeProviderException("The Workload API Socket endpoint address configured is not valid", e);
+ }
return new TrustManager[]{spiffeTrustManager};
}
diff --git a/java-spiffe-provider/src/main/java/spiffe/provider/X509SourceManager.java b/java-spiffe-provider/src/main/java/spiffe/provider/X509SourceManager.java
index 1c9ede6..2642d51 100644
--- a/java-spiffe-provider/src/main/java/spiffe/provider/X509SourceManager.java
+++ b/java-spiffe-provider/src/main/java/spiffe/provider/X509SourceManager.java
@@ -16,21 +16,32 @@ import spiffe.workloadapi.X509Source;
* to be used by the {@link SpiffeKeyManagerFactory} and {@link SpiffeTrustManagerFactory} to inject it
* in the {@link SpiffeKeyManager} and {@link SpiffeTrustManager} instances.
*/
-public enum X509SourceManager {
+public class X509SourceManager {
- INSTANCE;
+ private static volatile X509Source x509Source;
- private final X509Source x509Source;
-
- X509SourceManager() {
- try {
- x509Source = X509Source.newSource();
- } catch (SocketEndpointAddressException e) {
- throw new X509SourceException("Could not create X509 Source. Socket endpoint address is not valid", e);
- }
+ public X509SourceManager() {
}
- public X509Source getX509Source() {
- return x509Source;
+ /**
+ * Returns the single instance handled by this singleton. If the instance has not been
+ * created yet, it creates a new X509Source and initializes the singleton in a thread safe way.
+ *
+ * @return a {@link X509Source}
+ * @throws X509SourceException if the X509 source could not be initialized
+ * @throws SocketEndpointAddressException is the socket endpoint address is not valid
+ */
+ public static X509Source getX509Source() throws X509SourceException, SocketEndpointAddressException {
+ X509Source localRef = x509Source;
+ if (localRef == null) {
+ synchronized (X509SourceManager.class) {
+ localRef = x509Source;
+ if (localRef == null) {
+ localRef = X509Source.newSource();
+ x509Source = localRef;
+ }
+ }
+ }
+ return localRef;
}
}
diff --git a/java-spiffe-provider/src/main/java/spiffe/provider/examples/HttpsClient.java b/java-spiffe-provider/src/main/java/spiffe/provider/examples/HttpsClient.java
index 48885c8..d69f566 100644
--- a/java-spiffe-provider/src/main/java/spiffe/provider/examples/HttpsClient.java
+++ b/java-spiffe-provider/src/main/java/spiffe/provider/examples/HttpsClient.java
@@ -2,6 +2,7 @@ package spiffe.provider.examples;
import lombok.val;
import spiffe.exception.SocketEndpointAddressException;
+import spiffe.exception.X509SourceException;
import spiffe.provider.SpiffeSslContextFactory;
import spiffe.provider.SpiffeSslContextFactory.SslContextOptions;
import spiffe.spiffeid.SpiffeId;
@@ -38,11 +39,11 @@ public class HttpsClient {
int serverPort;
public static void main(String[] args) {
- String spiffeSocket = "unix:/tmp/agent.sock";
+ String spiffeSocket = "unix:/tmp/agent2.sock";
HttpsClient httpsClient = new HttpsClient(4000, spiffeSocket, HttpsClient::listOfSpiffeIds);
try {
httpsClient.run();
- } catch (KeyManagementException | NoSuchAlgorithmException | IOException | SocketEndpointAddressException e) {
+ } catch (KeyManagementException | NoSuchAlgorithmException | IOException | SocketEndpointAddressException | X509SourceException e) {
throw new RuntimeException("Error starting Https Client", e);
}
}
@@ -53,7 +54,7 @@ public class HttpsClient {
this.acceptedSpiffeIdsListSupplier = acceptedSpiffeIdsListSupplier;
}
- void run() throws IOException, SocketEndpointAddressException, KeyManagementException, NoSuchAlgorithmException {
+ void run() throws IOException, SocketEndpointAddressException, KeyManagementException, NoSuchAlgorithmException, X509SourceException {
val sourceOptions = X509SourceOptions
.builder()
diff --git a/java-spiffe-provider/src/main/java/spiffe/provider/examples/HttpsServer.java b/java-spiffe-provider/src/main/java/spiffe/provider/examples/HttpsServer.java
index 3043f6d..1efc7e1 100644
--- a/java-spiffe-provider/src/main/java/spiffe/provider/examples/HttpsServer.java
+++ b/java-spiffe-provider/src/main/java/spiffe/provider/examples/HttpsServer.java
@@ -2,8 +2,11 @@ package spiffe.provider.examples;
import lombok.val;
import spiffe.exception.SocketEndpointAddressException;
+import spiffe.exception.X509SourceException;
+import spiffe.provider.SpiffeProviderException;
import spiffe.provider.SpiffeSslContextFactory;
import spiffe.provider.SpiffeSslContextFactory.SslContextOptions;
+import spiffe.provider.X509SourceManager;
import spiffe.workloadapi.X509Source;
import javax.net.ssl.SSLContext;
@@ -46,9 +49,9 @@ public class HttpsServer {
void run() throws IOException, KeyManagementException, NoSuchAlgorithmException {
X509Source x509Source = null;
try {
- x509Source = X509Source.newSource();
- } catch (SocketEndpointAddressException e) {
- throw new RuntimeException(e);
+ x509Source = X509SourceManager.getX509Source();
+ } catch (SocketEndpointAddressException | X509SourceException e) {
+ throw new SpiffeProviderException(e);
}
val sslContextOptions = SslContextOptions