Skip to content

Commit 3e77f3a

Browse files
SNOW-2117143 CRL caching (#2294)
1 parent ab7ea13 commit 3e77f3a

16 files changed

+1315
-31
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package net.snowflake.client.core.crl;
2+
3+
interface CRLCache {
4+
CRLCacheEntry get(String crlUrl);
5+
6+
void put(String crlUrl, CRLCacheEntry entry);
7+
8+
void cleanup();
9+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package net.snowflake.client.core.crl;
2+
3+
import java.security.cert.X509CRL;
4+
import java.time.Duration;
5+
import java.time.Instant;
6+
7+
class CRLCacheEntry {
8+
private final X509CRL crl;
9+
private final Instant downloadTime;
10+
11+
CRLCacheEntry(X509CRL crl, Instant downloadTime) {
12+
if (crl == null) {
13+
throw new IllegalArgumentException("CRL cannot be null");
14+
}
15+
if (downloadTime == null) {
16+
throw new IllegalArgumentException("Download time cannot be null");
17+
}
18+
this.crl = crl;
19+
this.downloadTime = downloadTime;
20+
}
21+
22+
X509CRL getCrl() {
23+
return crl;
24+
}
25+
26+
Instant getDownloadTime() {
27+
return downloadTime;
28+
}
29+
30+
boolean isCrlExpired(Instant time) {
31+
return crl.getNextUpdate() != null && crl.getNextUpdate().toInstant().isBefore(time);
32+
}
33+
34+
boolean isEvicted(Instant time, Duration cacheValidityTime) {
35+
return downloadTime.plus(cacheValidityTime).isBefore(time);
36+
}
37+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package net.snowflake.client.core.crl;
2+
3+
import java.security.cert.X509CRL;
4+
import java.time.Duration;
5+
import java.time.Instant;
6+
import java.util.concurrent.Executors;
7+
import java.util.concurrent.ScheduledExecutorService;
8+
import java.util.concurrent.ThreadFactory;
9+
import java.util.concurrent.TimeUnit;
10+
import net.snowflake.client.jdbc.SnowflakeSQLLoggedException;
11+
import net.snowflake.client.log.SFLogger;
12+
import net.snowflake.client.log.SFLoggerFactory;
13+
14+
/**
15+
* Cache manager that coordinates between in-memory and file-based CRL caches. Provides automatic
16+
* cleanup of expired entries and proper lifecycle management.
17+
*/
18+
class CRLCacheManager {
19+
20+
private static final SFLogger logger = SFLoggerFactory.getLogger(CRLCacheManager.class);
21+
22+
private final CRLCache memoryCache;
23+
private final CRLCache fileCache;
24+
private final ScheduledExecutorService cleanupScheduler;
25+
private final Runnable cleanupTask;
26+
private final long cleanupIntervalInMs;
27+
28+
CRLCacheManager(CRLCache memoryCache, CRLCache fileCache, Duration cleanupInterval) {
29+
this.memoryCache = memoryCache;
30+
this.fileCache = fileCache;
31+
this.cleanupIntervalInMs = cleanupInterval.toMillis();
32+
33+
this.cleanupTask =
34+
() -> {
35+
try {
36+
logger.debug(
37+
"Running periodic CRL cache cleanup with interval {} seconds",
38+
cleanupIntervalInMs / 1000.0);
39+
memoryCache.cleanup();
40+
fileCache.cleanup();
41+
} catch (Exception e) {
42+
logger.error("An error occurred during scheduled CRL cache cleanup.", e);
43+
}
44+
};
45+
ThreadFactory threadFactory =
46+
r -> {
47+
Thread t = new Thread(r, "crl-cache-cleanup");
48+
t.setDaemon(true); // Don't prevent JVM shutdown
49+
return t;
50+
};
51+
this.cleanupScheduler = Executors.newSingleThreadScheduledExecutor(threadFactory);
52+
}
53+
54+
static CRLCacheManager fromConfig(CRLValidationConfig config) throws SnowflakeSQLLoggedException {
55+
CRLCache memoryCache;
56+
if (config.isInMemoryCacheEnabled()) {
57+
logger.debug("Enabling in-memory CRL cache");
58+
memoryCache = new CRLInMemoryCache(config.getCacheValidityTime());
59+
} else {
60+
logger.debug("In-memory CRL cache disabled");
61+
memoryCache = NoopCRLCache.INSTANCE;
62+
}
63+
64+
CRLCache fileCache;
65+
if (config.isOnDiskCacheEnabled()) {
66+
logger.debug("Enabling file based CRL cache");
67+
fileCache = new CRLFileCache(config.getOnDiskCacheDir(), config.getOnDiskCacheRemovalDelay());
68+
} else {
69+
logger.debug("File based CRL cache disabled");
70+
fileCache = NoopCRLCache.INSTANCE;
71+
}
72+
73+
CRLCacheManager manager =
74+
new CRLCacheManager(memoryCache, fileCache, config.getOnDiskCacheRemovalDelay());
75+
if (config.isInMemoryCacheEnabled() || config.isOnDiskCacheEnabled()) {
76+
manager.startPeriodicCleanup();
77+
}
78+
return manager;
79+
}
80+
81+
CRLCacheEntry get(String crlUrl) {
82+
CRLCacheEntry entry = memoryCache.get(crlUrl);
83+
if (entry != null) {
84+
return entry;
85+
}
86+
87+
entry = fileCache.get(crlUrl);
88+
if (entry != null) {
89+
// Promote to memory cache
90+
memoryCache.put(crlUrl, entry);
91+
return entry;
92+
}
93+
94+
logger.debug("CRL not found in cache for {}", crlUrl);
95+
return null;
96+
}
97+
98+
void put(String crlUrl, X509CRL crl, Instant downloadTime) {
99+
CRLCacheEntry entry = new CRLCacheEntry(crl, downloadTime);
100+
memoryCache.put(crlUrl, entry);
101+
fileCache.put(crlUrl, entry);
102+
}
103+
104+
private void startPeriodicCleanup() {
105+
cleanupScheduler.scheduleAtFixedRate(
106+
cleanupTask, cleanupIntervalInMs, cleanupIntervalInMs, TimeUnit.MILLISECONDS);
107+
108+
logger.debug(
109+
"Scheduled CRL cache cleanup task to run every {} seconds.", cleanupIntervalInMs / 1000.0);
110+
}
111+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package net.snowflake.client.core.crl;
2+
3+
import java.io.IOException;
4+
import java.io.InputStream;
5+
import java.io.UnsupportedEncodingException;
6+
import java.net.URLEncoder;
7+
import java.nio.charset.StandardCharsets;
8+
import java.nio.file.Files;
9+
import java.nio.file.Path;
10+
import java.nio.file.StandardOpenOption;
11+
import java.nio.file.attribute.BasicFileAttributes;
12+
import java.nio.file.attribute.FileTime;
13+
import java.nio.file.attribute.PosixFilePermissions;
14+
import java.security.cert.CRLException;
15+
import java.security.cert.CertificateException;
16+
import java.security.cert.CertificateFactory;
17+
import java.security.cert.X509CRL;
18+
import java.time.Duration;
19+
import java.time.Instant;
20+
import java.util.concurrent.locks.Lock;
21+
import java.util.concurrent.locks.ReentrantLock;
22+
import java.util.stream.Collectors;
23+
import java.util.stream.Stream;
24+
import net.snowflake.client.core.Constants;
25+
import net.snowflake.client.jdbc.SnowflakeSQLLoggedException;
26+
import net.snowflake.client.log.SFLogger;
27+
import net.snowflake.client.log.SFLoggerFactory;
28+
import net.snowflake.common.core.SqlState;
29+
30+
class CRLFileCache implements CRLCache {
31+
32+
private static final SFLogger logger = SFLoggerFactory.getLogger(CRLFileCache.class);
33+
34+
private final Path cacheDir;
35+
private final Duration removalDelay;
36+
private final Lock cacheLock = new ReentrantLock();
37+
38+
CRLFileCache(Path cacheDir, Duration removalDelay) throws SnowflakeSQLLoggedException {
39+
this.cacheDir = cacheDir;
40+
this.removalDelay = removalDelay;
41+
42+
ensureCacheDirectoryExists(cacheDir);
43+
}
44+
45+
public CRLCacheEntry get(String crlUrl) {
46+
try {
47+
cacheLock.lock();
48+
Path crlFilePath = getCrlFilePath(crlUrl);
49+
if (Files.exists(crlFilePath)) {
50+
logger.debug("Found CRL on disk for {}", crlFilePath);
51+
52+
BasicFileAttributes attrs = Files.readAttributes(crlFilePath, BasicFileAttributes.class);
53+
Instant downloadTime = attrs.lastModifiedTime().toInstant();
54+
55+
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
56+
try (InputStream crlBytes = Files.newInputStream(crlFilePath)) {
57+
X509CRL crl = (X509CRL) certFactory.generateCRL(crlBytes);
58+
return new CRLCacheEntry(crl, downloadTime);
59+
}
60+
}
61+
} catch (Exception e) {
62+
logger.warn("Failed to read CRL from disk cache for {}: {}", crlUrl, e.getMessage());
63+
} finally {
64+
cacheLock.unlock();
65+
}
66+
67+
return null;
68+
}
69+
70+
public void put(String crlUrl, CRLCacheEntry entry) {
71+
try {
72+
cacheLock.lock();
73+
Path crlFilePath = getCrlFilePath(crlUrl);
74+
75+
Files.write(
76+
crlFilePath,
77+
entry.getCrl().getEncoded(),
78+
StandardOpenOption.CREATE,
79+
StandardOpenOption.WRITE,
80+
StandardOpenOption.TRUNCATE_EXISTING);
81+
82+
Files.setLastModifiedTime(crlFilePath, FileTime.from(entry.getDownloadTime()));
83+
84+
if (Constants.getOS() != Constants.OS.WINDOWS) {
85+
Files.setPosixFilePermissions(crlFilePath, PosixFilePermissions.fromString("rw-------"));
86+
}
87+
88+
logger.debug("Updated disk cache for {}", crlUrl);
89+
} catch (Exception e) {
90+
logger.warn("Failed to write CRL to disk cache for {}: {}", crlUrl, e.getMessage());
91+
} finally {
92+
cacheLock.unlock();
93+
}
94+
}
95+
96+
public void cleanup() {
97+
Instant now = Instant.now();
98+
logger.debug("Cleaning up on-disk CRL cache at {}", now);
99+
100+
try {
101+
if (!Files.exists(cacheDir)) {
102+
return;
103+
}
104+
105+
int removedCount = 0;
106+
try (Stream<Path> files = Files.list(cacheDir)) {
107+
cacheLock.lock();
108+
for (Path filePath : files.filter(Files::isRegularFile).collect(Collectors.toList())) {
109+
try {
110+
try (InputStream crlBytes = Files.newInputStream(filePath)) {
111+
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
112+
X509CRL crl = (X509CRL) certFactory.generateCRL(crlBytes);
113+
CRLCacheEntry entry =
114+
new CRLCacheEntry(crl, Files.getLastModifiedTime(filePath).toInstant());
115+
116+
boolean expired = entry.isCrlExpired(now);
117+
boolean evicted = entry.isEvicted(now, removalDelay);
118+
if (expired || evicted) {
119+
Files.delete(filePath);
120+
removedCount++;
121+
logger.debug(
122+
"Removing file based CRL cache entry for {}: expired={}, evicted={}",
123+
filePath,
124+
expired,
125+
evicted);
126+
}
127+
}
128+
} catch (IOException | CRLException | CertificateException e) {
129+
// If we can't parse the file, it's probably corrupted - remove it
130+
try {
131+
Files.delete(filePath);
132+
removedCount++;
133+
} catch (IOException deleteError) {
134+
logger.warn(
135+
"Failed to delete corrupted CRL file {}: {}", filePath, deleteError.getMessage());
136+
}
137+
}
138+
}
139+
} finally {
140+
cacheLock.unlock();
141+
}
142+
143+
if (removedCount > 0) {
144+
logger.debug("Removed {} expired/corrupted files from disk CRL cache", removedCount);
145+
}
146+
} catch (Exception e) {
147+
logger.warn("Failed to cleanup disk CRL cache: {}", e.getMessage());
148+
}
149+
}
150+
151+
private Path getCrlFilePath(String crlUrl) throws UnsupportedEncodingException {
152+
String encodedUrl = URLEncoder.encode(crlUrl, StandardCharsets.UTF_8.toString());
153+
return cacheDir.resolve(encodedUrl);
154+
}
155+
156+
private static boolean ownerOnlyPermissions(Path cacheDir) throws IOException {
157+
return Files.getPosixFilePermissions(cacheDir)
158+
.equals(PosixFilePermissions.fromString("rwx------"));
159+
}
160+
161+
private static void ensureCacheDirectoryExists(Path cacheDir) throws SnowflakeSQLLoggedException {
162+
try {
163+
boolean exists = Files.exists(cacheDir);
164+
if (!exists) {
165+
Files.createDirectories(cacheDir);
166+
logger.debug("Initialized CRL cache directory: {}", cacheDir);
167+
}
168+
169+
if (Constants.getOS() != Constants.OS.WINDOWS && !ownerOnlyPermissions(cacheDir)) {
170+
Files.setPosixFilePermissions(cacheDir, PosixFilePermissions.fromString("rwx------"));
171+
logger.debug("Set CRL cache directory permissions to 'rwx------");
172+
}
173+
} catch (Exception e) {
174+
throw new SnowflakeSQLLoggedException(
175+
null, null, SqlState.INTERNAL_ERROR, "Failed to create CRL cache directory", e);
176+
}
177+
}
178+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package net.snowflake.client.core.crl;
2+
3+
import java.time.Duration;
4+
import java.time.Instant;
5+
import java.util.concurrent.ConcurrentHashMap;
6+
import net.snowflake.client.log.SFLogger;
7+
import net.snowflake.client.log.SFLoggerFactory;
8+
9+
class CRLInMemoryCache implements CRLCache {
10+
11+
private static final SFLogger logger = SFLoggerFactory.getLogger(CRLInMemoryCache.class);
12+
private final ConcurrentHashMap<String, CRLCacheEntry> cache = new ConcurrentHashMap<>();
13+
private final Duration cacheValidityTime;
14+
15+
CRLInMemoryCache(Duration cacheValidityTime) {
16+
this.cacheValidityTime = cacheValidityTime;
17+
}
18+
19+
public CRLCacheEntry get(String crlUrl) {
20+
CRLCacheEntry entry = cache.get(crlUrl);
21+
if (entry != null) {
22+
logger.debug("Found CRL in memory cache for {}", crlUrl);
23+
}
24+
return entry;
25+
}
26+
27+
public void put(String crlUrl, CRLCacheEntry entry) {
28+
cache.put(crlUrl, entry);
29+
}
30+
31+
public void cleanup() {
32+
Instant now = Instant.now();
33+
logger.debug("Cleaning up in-memory CRL cache at {}", now);
34+
35+
int initialSize = cache.size();
36+
cache
37+
.entrySet()
38+
.removeIf(
39+
entry -> {
40+
CRLCacheEntry cacheEntry = entry.getValue();
41+
boolean expired = cacheEntry.isCrlExpired(now);
42+
boolean evicted = cacheEntry.isEvicted(now, cacheValidityTime);
43+
logger.debug(
44+
"Removing in-memory CRL cache entry for {}: expired={}, evicted={}",
45+
entry.getKey(),
46+
expired,
47+
evicted);
48+
return expired || evicted;
49+
});
50+
51+
int removedCount = initialSize - cache.size();
52+
if (removedCount > 0) {
53+
logger.debug("Removed {} expired/evicted entries from in-memory CRL cache", removedCount);
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)