Skip to content

Commit

Permalink
Implement Wallet Expiration Parsing and Resource Caching Enhancement (#…
Browse files Browse the repository at this point in the history
…130)

* Implement Wallet Expiration Parsing and Resource Caching Enhancement

* Remove unused imports

* Remove unused imports

* test parse Expirating Date

* test parse Expirating Date

* test parse Expirating Date

* test parse Expirating Date

* test parse Expirating Date

* Fix Date Parsing for Wallet Expiration Date and Handle Time Zone Consistency

* Address comment review

* Optimize Wallet expiration date handling
  • Loading branch information
MouhsinElmajdouby authored Dec 16, 2024
1 parent 190a9c0 commit fadb0c6
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,24 @@
package oracle.jdbc.provider.util;

import javax.net.ssl.SSLContext;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.KeyStore;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
* <p>
* Represents a wallet zip generated by the ADB service. The contents of the zip
Expand All @@ -59,6 +72,11 @@
* JDBC requires a dependency on the Oracle PKI security provider. The JKS file
* is chosen over the SSO or PKCS12 files in order to avoid this dependency.
* </p>
* <p>
* It also extracts the expiration date of the wallet's certificates from
* the {@code README} file included in the wallet zip. The expiration date is
* used to determine when the wallet should be refreshed.
* </p>
*/
public final class Wallet {

Expand All @@ -74,18 +92,42 @@ public final class Wallet {
/** Type of the keystore file in a wallet zip */
private static final String KEY_STORE_TYPE = "JKS";

/** Name of the README file in a wallet zip */
private static final String README_FILE = "README";

/** Pattern to match the expiration date line in the README file */
private static final Pattern EXPIRY_PATTERN = Pattern.compile(
"The SSL certificates provided in this wallet will expire on ([\\d\\-\\.: ]+ UTC)\\.");

/** Contents of the tnsnames.ora file */
private final TNSNames tnsNames;

/** Expiration date of the wallet's certificates, extracted from the
* README file */
private final OffsetDateTime expirationDate;

/**
* An {@code SSLContext} initialized with key and trust material of the
* keystore.jks and truststore.jks files.
*/
private final SSLContext sslContext;

private Wallet(TNSNames tnsNames, SSLContext sslContext) {
/**
* A static DateTimeFormatter used to parse expiration dates in the README.
*/
private static final DateTimeFormatter EXPIRATION_DATE_FORMATTER =
new DateTimeFormatterBuilder()
.appendPattern("yyyy-MM-dd HH:mm:ss")
.optionalStart()
.appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
.optionalEnd()
.appendPattern("X") // Accept 'Z' as offset
.toFormatter(Locale.ENGLISH);

private Wallet(TNSNames tnsNames, SSLContext sslContext, OffsetDateTime expirationDate) {
this.tnsNames = tnsNames;
this.sslContext = sslContext;
this.expirationDate = expirationDate;
}

/**
Expand All @@ -96,6 +138,13 @@ public SSLContext getSSLContext() {
return sslContext;
}

/**
* @return The expiration date of the wallet's certificates, or null if not available.
*/
public OffsetDateTime getExpirationDate() {
return expirationDate;
}

/**
* @return The connection string for high consumer group. Not null.
* @throws IllegalStateException If no connection string is defined for the
Expand Down Expand Up @@ -162,6 +211,9 @@ private String getConnectionString(TNSNames.ConsumerGroup consumerGroup) {
/**
* Unzips the stream of a wallet directory, returning a {@code Wallet} that
* retains the contents of any files relevant to Oracle JDBC.
* @param zipStream The input stream of the wallet zip file.
* @param password The password used to decrypt the keystore.
* @return A new {@code Wallet} instance containing the parsed wallet data.
* @throws IllegalStateException If the files are not found or can not be
* decoded.
*/
Expand All @@ -170,6 +222,7 @@ public static Wallet unzip(ZipInputStream zipStream, char[] password) {
TNSNames tnsNames = null;
KeyStore keyStore = null;
KeyStore trustStore = null;
OffsetDateTime expirationDate = null;

try {
for (ZipEntry entry = zipStream.getNextEntry(); entry != null; entry = zipStream.getNextEntry()) {
Expand All @@ -185,6 +238,9 @@ public static Wallet unzip(ZipInputStream zipStream, char[] password) {
trustStore =
TlsUtils.loadKeyStore(zipStream, null, KEY_STORE_TYPE, null);
break;
case README_FILE:
expirationDate = parseExpirationDateFromReadme(zipStream);
break;
default:
// Ignore other files
}
Expand All @@ -205,12 +261,76 @@ public static Wallet unzip(ZipInputStream zipStream, char[] password) {

return new Wallet(
tnsNames,
TlsUtils.createSSLContext(keyStore, trustStore, password));
TlsUtils.createSSLContext(keyStore, trustStore, password),
expirationDate);
}

/** Returns an exception for a missing file in the wallet ZIP */
private static IllegalStateException missingFile(String fileName) {
return new IllegalStateException("Wallet ZIP did not contain: " + fileName);
}

/**
* Reads an {@code InputStream} line by line to find and extract the expiration
* date of the wallet's certificates using a predefined pattern.
* Stops reading as soon as the expiration date is found.
*
* @param inputStream The input stream to read from. Not null.
* @return The extracted expiration date string, or {@code null}
* if no match is found.
* @throws IOException If an I/O error occurs while reading the stream.
*/
private static String findExpirationDateInStream(InputStream inputStream)
throws IOException {
BufferedReader reader = new BufferedReader(
new InputStreamReader(inputStream, UTF_8));
String line;
while ((line = reader.readLine()) != null) {
Matcher matcher = EXPIRY_PATTERN.matcher(line);
if (matcher.find()) {
return matcher.group(1).trim().replace(" UTC", "Z");
}
}
return null;
}

/**
* Parses the expiration date of the wallet's certificates from the
* content of a {@code README} file provided as an {@code InputStream}.
* The method stops reading the stream as soon as the expiration date
* is found and attempts to parse it into an {@code OffsetDateTime}.
*
* @param inputStream The input stream of the {@code README} file.
* @return The parsed expiration date as an {@code OffsetDateTime},
* or {@code null} if no expiration date is found or if parsing fails.
* @throws IOException If an I/O error occurs while reading the stream.
*/
public static OffsetDateTime parseExpirationDateFromReadme(InputStream inputStream)
throws IOException {
if (inputStream == null) {
return null;
}
String expiryDateString = findExpirationDateInStream(inputStream);
if (expiryDateString == null) {
return null;
}
return parseOffsetDateTime(expiryDateString);
}

/**
* Parses a date-time string into an OffsetDateTime.
* The method uses the predefined {@code EXPIRATION_DATE_FORMATTER} to parse
* the given string. If parsing fails due to an invalid format or other reasons,
* the method returns {@code null}.
*
* @param dateTimeString The date-time string to parse.
* @return The parsed OffsetDateTime, or null if parsing fails.
*/
private static OffsetDateTime parseOffsetDateTime(String dateTimeString) {
try {
return OffsetDateTime.parse(dateTimeString, EXPIRATION_DATE_FORMATTER);
} catch (DateTimeParseException e) {
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@

import java.io.IOException;
import java.io.InputStream;
import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.zip.ZipInputStream;

Expand All @@ -75,8 +76,6 @@ private WalletFactory() { }

/**
* A cache of wallet configurations requested from the OCI database service.
* TODO: The cache should invalidate entries after TLS certificates have
* expired.
*/
private static final ResourceFactory<Wallet> INSTANCE =
CachedResourceFactory.create(new WalletFactory());
Expand Down Expand Up @@ -132,9 +131,13 @@ protected Resource<Wallet> request(
"Failed to close ZIP stream", ioException);
}

// TODO: It may be possible to parse an expiration time from the wallet
// README, and return an expiring resource here.
return Resource.createPermanentResource(wallet, false);
OffsetDateTime expiry = wallet.getExpirationDate();
if (expiry == null) {
// If expiry could not be determined, treat as permanent
return Resource.createPermanentResource(wallet, false);
} else {
return Resource.createExpiringResource(wallet, expiry, false);
}
}
finally {
Arrays.fill(password, (char)0);
Expand Down

0 comments on commit fadb0c6

Please sign in to comment.