Skip to content

Commit a54fd30

Browse files
committed
harden caller authorization, bound async queues, and guard Redis messaging
1 parent 6c809e4 commit a54fd30

18 files changed

Lines changed: 263 additions & 48 deletions

File tree

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>nl.hauntedmc.dataprovider</groupId>
88
<artifactId>dataprovider</artifactId>
9-
<version>1.20.1</version>
9+
<version>1.20.2</version>
1010
<packaging>jar</packaging>
1111
<name>DataProvider</name>
1212
<url>http://maven.apache.org</url>

src/main/java/nl/hauntedmc/dataprovider/database/document/impl/mongodb/MongoDBDatabase.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.mongodb.MongoClientSettings;
55
import com.mongodb.client.MongoClient;
66
import com.mongodb.client.MongoClients;
7+
import nl.hauntedmc.dataprovider.internal.concurrent.BoundedExecutorFactory;
78
import nl.hauntedmc.dataprovider.database.document.DocumentDataAccess;
89
import nl.hauntedmc.dataprovider.database.document.DocumentDatabaseProvider;
910
import nl.hauntedmc.dataprovider.database.security.TlsSupport;
@@ -16,7 +17,6 @@
1617
import java.nio.charset.StandardCharsets;
1718
import java.util.Objects;
1819
import java.util.concurrent.ExecutorService;
19-
import java.util.concurrent.Executors;
2020
import java.util.concurrent.TimeUnit;
2121

2222
/**
@@ -106,7 +106,8 @@ public void connect() {
106106
createdClient.getDatabase(configuredDatabaseName).runCommand(new Document("ping", 1));
107107

108108
final int poolSize = Math.max(1, config.node("pool_size").getInt(8));
109-
createdExecutor = Executors.newFixedThreadPool(poolSize);
109+
final int queueCapacity = Math.max(poolSize, config.node("queue_capacity").getInt(poolSize * 200));
110+
createdExecutor = BoundedExecutorFactory.create("dataprovider-mongodb", poolSize, queueCapacity);
110111

111112
mongoClient = createdClient;
112113
executor = createdExecutor;
@@ -115,10 +116,11 @@ public void connect() {
115116

116117
connected = true;
117118
logger.info(String.format(
118-
"[MongoDBDatabase] Connected successfully to Mongo at %s:%d (tls=%s)",
119+
"[MongoDBDatabase] Connected successfully to Mongo at %s:%d (tls=%s, queueCapacity=%d)",
119120
host,
120121
port,
121-
tlsEnabled ? "enabled" : "disabled"
122+
tlsEnabled ? "enabled" : "disabled",
123+
queueCapacity
122124
));
123125
} catch (Exception e) {
124126
if (createdExecutor != null) {

src/main/java/nl/hauntedmc/dataprovider/database/keyvalue/impl/memcached/MemcachedDatabase.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package nl.hauntedmc.dataprovider.database.keyvalue.impl.memcached;
22

3+
import nl.hauntedmc.dataprovider.internal.concurrent.BoundedExecutorFactory;
34
import nl.hauntedmc.dataprovider.database.keyvalue.KeyValueDataAccess;
45
import nl.hauntedmc.dataprovider.database.keyvalue.KeyValueDatabaseProvider;
56
import nl.hauntedmc.dataprovider.platform.common.logger.ILoggerAdapter;
@@ -9,7 +10,6 @@
910
import java.net.InetSocketAddress;
1011
import java.util.Objects;
1112
import java.util.concurrent.ExecutorService;
12-
import java.util.concurrent.Executors;
1313
import java.util.concurrent.TimeUnit;
1414

1515
/**
@@ -41,14 +41,15 @@ public void connect() {
4141
final String host = config.node("host").getString("localhost");
4242
final int port = config.node("port").getInt(11211);
4343
final int poolSize = Math.max(1, config.node("pool_size").getInt(8));
44+
final int queueCapacity = Math.max(poolSize, config.node("queue_capacity").getInt(poolSize * 200));
4445

4546
createdClient = new MemcachedClient(new InetSocketAddress(host, port));
4647
if (createdClient.getAvailableServers().isEmpty()) {
4748
throw new IllegalStateException("Memcached has no available servers after connect.");
4849
}
4950
logger.info(String.format("[MemcachedDatabase] Connected to Memcached at %s:%d", host, port));
5051

51-
createdExecutor = Executors.newFixedThreadPool(poolSize);
52+
createdExecutor = BoundedExecutorFactory.create("dataprovider-memcached", poolSize, queueCapacity);
5253

5354
memcachedClient = createdClient;
5455
executor = createdExecutor;

src/main/java/nl/hauntedmc/dataprovider/database/keyvalue/impl/redis/RedisDatabase.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package nl.hauntedmc.dataprovider.database.keyvalue.impl.redis;
22

3+
import nl.hauntedmc.dataprovider.internal.concurrent.BoundedExecutorFactory;
34
import nl.hauntedmc.dataprovider.database.keyvalue.KeyValueDataAccess;
45
import nl.hauntedmc.dataprovider.database.keyvalue.KeyValueDatabaseProvider;
56
import nl.hauntedmc.dataprovider.database.security.TlsSupport;
@@ -12,7 +13,6 @@
1213

1314
import java.util.Objects;
1415
import java.util.concurrent.ExecutorService;
15-
import java.util.concurrent.Executors;
1616
import java.util.concurrent.TimeUnit;
1717

1818
/**
@@ -48,6 +48,7 @@ public void connect() {
4848
final int databaseIndex = config.node("database").getInt(0);
4949
final int poolSize = Math.max(1,
5050
config.node("pool_size").getInt(config.node("pool", "connections").getInt(8)));
51+
final int queueCapacity = Math.max(poolSize, config.node("queue_capacity").getInt(poolSize * 200));
5152
final boolean tlsEnabled = config.node("tls", "enabled").getBoolean(false);
5253
final boolean verifyHostname = config.node("tls", "verify_hostname").getBoolean(true);
5354
final boolean trustAllCertificates = config.node("tls", "trust_all_certificates").getBoolean(false);
@@ -88,21 +89,22 @@ public void connect() {
8889
}
8990
}
9091

91-
createdExecutor = Executors.newFixedThreadPool(poolSize);
92+
createdExecutor = BoundedExecutorFactory.create("dataprovider-redis", poolSize, queueCapacity);
9293

9394
jedisPool = createdPool;
9495
executor = createdExecutor;
9596
dataAccess = new RedisDataAccess(jedisPool, executor);
9697

9798
connected = true;
9899
logger.info(String.format(
99-
"[RedisDatabase] Connected to Redis at %s:%d (DB %d, auth=%s, tls=%s), poolSize=%d",
100+
"[RedisDatabase] Connected to Redis at %s:%d (DB %d, auth=%s, tls=%s), poolSize=%d, queueCapacity=%d",
100101
host,
101102
port,
102103
databaseIndex,
103104
(password != null && !password.isBlank()) ? "enabled" : "disabled",
104105
tlsEnabled ? "enabled" : "disabled",
105-
poolSize
106+
poolSize,
107+
queueCapacity
106108
));
107109
} catch (Exception e) {
108110
if (createdExecutor != null) {

src/main/java/nl/hauntedmc/dataprovider/database/messaging/impl/redis/RedisMessagingDataAccess.java

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,18 @@
1717
import java.util.concurrent.atomic.AtomicBoolean;
1818
import java.util.concurrent.atomic.AtomicLong;
1919
import java.util.function.Consumer;
20+
import java.util.regex.Pattern;
2021

2122
final class RedisMessagingDataAccess implements MessagingDataAccess {
2223

24+
private static final Pattern DESTINATION_PATTERN = Pattern.compile("[A-Za-z0-9_.:-]{1,128}");
25+
2326
private final JedisPool pool;
2427
private final ExecutorService workers;
2528
private final ILoggerAdapter logger;
2629
private final MessageRegistry messageRegistry;
2730
private final int maxSubscriptions;
31+
private final int maxPayloadChars;
2832
private final Map<String, ChannelSubscription> channelSubscriptions = new ConcurrentHashMap<>();
2933
private final Object subscriptionLock = new Object();
3034
private final AtomicBoolean shuttingDown = new AtomicBoolean(false);
@@ -34,7 +38,8 @@ final class RedisMessagingDataAccess implements MessagingDataAccess {
3438
ExecutorService workers,
3539
ILoggerAdapter logger,
3640
MessageRegistry messageRegistry,
37-
int maxSubscriptions
41+
int maxSubscriptions,
42+
int maxPayloadChars
3843
) {
3944
this.pool = Objects.requireNonNull(pool, "Pool cannot be null");
4045
this.workers = Objects.requireNonNull(workers, "Workers cannot be null");
@@ -44,21 +49,29 @@ final class RedisMessagingDataAccess implements MessagingDataAccess {
4449
throw new IllegalArgumentException("maxSubscriptions must be greater than zero");
4550
}
4651
this.maxSubscriptions = maxSubscriptions;
52+
if (maxPayloadChars < 1) {
53+
throw new IllegalArgumentException("maxPayloadChars must be greater than zero");
54+
}
55+
this.maxPayloadChars = maxPayloadChars;
4756
}
4857

4958
@Override
5059
public <T extends EventMessage> CompletableFuture<Void> publish(String dest, T msg) {
51-
Objects.requireNonNull(dest, "Destination cannot be null");
60+
String destination = validateDestination(dest);
5261
Objects.requireNonNull(msg, "Message cannot be null");
5362

5463
if (shuttingDown.get()) {
5564
return CompletableFuture.failedFuture(new IllegalStateException("Messaging provider is shutting down"));
5665
}
5766

5867
String json = messageRegistry.toJson(msg);
68+
if (json.length() > maxPayloadChars) {
69+
return CompletableFuture.failedFuture(new IllegalArgumentException(
70+
"Message payload exceeds maxPayloadChars (" + maxPayloadChars + ")"));
71+
}
5972
return CompletableFuture.runAsync(() -> {
6073
try (Jedis j = pool.getResource()) {
61-
j.publish(dest, json);
74+
j.publish(destination, json);
6275
}
6376
}, workers);
6477
}
@@ -67,7 +80,7 @@ public <T extends EventMessage> CompletableFuture<Void> publish(String dest, T m
6780
public <T extends EventMessage> Subscription subscribe(
6881
String dest, Class<T> type, Consumer<T> handler
6982
) {
70-
Objects.requireNonNull(dest, "Destination cannot be null");
83+
String destination = validateDestination(dest);
7184
Objects.requireNonNull(type, "Type cannot be null");
7285
Objects.requireNonNull(handler, "Handler cannot be null");
7386

@@ -78,13 +91,13 @@ public <T extends EventMessage> Subscription subscribe(
7891
ChannelSubscription channelSubscription;
7992
boolean created = false;
8093
synchronized (subscriptionLock) {
81-
channelSubscription = channelSubscriptions.get(dest);
94+
channelSubscription = channelSubscriptions.get(destination);
8295
if (channelSubscription == null) {
8396
if (channelSubscriptions.size() >= maxSubscriptions) {
8497
throw new IllegalStateException("Maximum active Redis subscriptions reached (" + maxSubscriptions + ")");
8598
}
86-
channelSubscription = new ChannelSubscription(dest);
87-
channelSubscriptions.put(dest, channelSubscription);
99+
channelSubscription = new ChannelSubscription(destination);
100+
channelSubscriptions.put(destination, channelSubscription);
88101
created = true;
89102
}
90103
}
@@ -126,6 +139,15 @@ private ChannelSubscription(String destination) {
126139
this.pubSub = new JedisPubSub() {
127140
@Override
128141
public void onMessage(String channel, String raw) {
142+
if (raw == null || raw.isBlank()) {
143+
logger.warn("Received empty message while subscribing to channel " + channel);
144+
return;
145+
}
146+
if (raw.length() > maxPayloadChars) {
147+
logger.warn("Dropped oversized message on channel " + channel + " (length="
148+
+ raw.length() + ", maxPayloadChars=" + maxPayloadChars + ")");
149+
return;
150+
}
129151
for (HandlerRegistration<?> registration : handlers.values()) {
130152
registration.dispatch(channel, raw);
131153
}
@@ -214,4 +236,12 @@ private void dispatch(String channel, String raw) {
214236
}
215237
}
216238
}
239+
240+
private static String validateDestination(String destination) {
241+
Objects.requireNonNull(destination, "Destination cannot be null");
242+
if (!DESTINATION_PATTERN.matcher(destination).matches()) {
243+
throw new IllegalArgumentException("Destination contains unsupported characters.");
244+
}
245+
return destination;
246+
}
217247
}

src/main/java/nl/hauntedmc/dataprovider/database/messaging/impl/redis/RedisMessagingDatabase.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package nl.hauntedmc.dataprovider.database.messaging.impl.redis;
22

3+
import nl.hauntedmc.dataprovider.internal.concurrent.BoundedExecutorFactory;
34
import nl.hauntedmc.dataprovider.database.messaging.MessagingDataAccess;
45
import nl.hauntedmc.dataprovider.database.messaging.MessagingDatabaseProvider;
56
import nl.hauntedmc.dataprovider.database.messaging.api.MessageRegistry;
@@ -10,7 +11,6 @@
1011

1112
import java.util.Objects;
1213
import java.util.concurrent.ExecutorService;
13-
import java.util.concurrent.Executors;
1414
import java.util.concurrent.TimeUnit;
1515

1616
/**
@@ -43,7 +43,9 @@ public synchronized void connect() {
4343
String pass = cfg.node("password").getString("");
4444
int connectionPoolSize = Math.max(1, cfg.node("pool", "connections").getInt(4));
4545
int workerPoolSize = Math.max(1, cfg.node("pool", "threads").getInt(8));
46+
int workerQueueCapacity = Math.max(workerPoolSize, cfg.node("pool", "queue_capacity").getInt(workerPoolSize * 200));
4647
int maxSubscriptions = Math.max(1, cfg.node("pool", "max_subscriptions").getInt(64));
48+
int maxPayloadChars = Math.max(256, cfg.node("security", "max_payload_chars").getInt(32_768));
4749
boolean tlsEnabled = cfg.node("tls", "enabled").getBoolean(false);
4850
boolean verifyHostname = cfg.node("tls", "verify_hostname").getBoolean(true);
4951
boolean trustAllCertificates = cfg.node("tls", "trust_all_certificates").getBoolean(false);
@@ -82,18 +84,20 @@ public synchronized void connect() {
8284
jedis.ping();
8385
}
8486

85-
workers = Executors.newFixedThreadPool(workerPoolSize);
86-
bus = new RedisMessagingDataAccess(pool, workers, logger, messageRegistry, maxSubscriptions);
87+
workers = BoundedExecutorFactory.create("dataprovider-redis-msg", workerPoolSize, workerQueueCapacity);
88+
bus = new RedisMessagingDataAccess(pool, workers, logger, messageRegistry, maxSubscriptions, maxPayloadChars);
8789
connected = true;
8890

8991
logger.info(String.format(
90-
"[RedisMessagingDatabase] Connected to Redis messaging at %s:%d (db=%d, auth=%s, tls=%s, maxSubscriptions=%d)",
92+
"[RedisMessagingDatabase] Connected to Redis messaging at %s:%d (db=%d, auth=%s, tls=%s, maxSubscriptions=%d, queueCapacity=%d, maxPayloadChars=%d)",
9193
host,
9294
port,
9395
db,
9496
pass.isEmpty() ? "disabled" : "enabled",
9597
tlsEnabled ? "enabled" : "disabled",
96-
maxSubscriptions
98+
maxSubscriptions,
99+
workerQueueCapacity,
100+
maxPayloadChars
97101
));
98102
} catch (Exception e) {
99103
connected = false;

src/main/java/nl/hauntedmc/dataprovider/database/relational/impl/mysql/MySQLDatabase.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.zaxxer.hikari.HikariConfig;
44
import com.zaxxer.hikari.HikariDataSource;
5+
import nl.hauntedmc.dataprovider.internal.concurrent.BoundedExecutorFactory;
56
import nl.hauntedmc.dataprovider.database.relational.RelationalDataAccess;
67
import nl.hauntedmc.dataprovider.database.relational.RelationalDatabaseProvider;
78
import nl.hauntedmc.dataprovider.database.relational.schema.SchemaManager;
@@ -13,7 +14,6 @@
1314
import java.util.Objects;
1415
import java.util.Set;
1516
import java.util.concurrent.ExecutorService;
16-
import java.util.concurrent.Executors;
1717
import java.util.concurrent.TimeUnit;
1818

1919
/**
@@ -79,14 +79,15 @@ public void connect() {
7979
hikariConfig.setPassword(password);
8080

8181
final int poolSize = Math.max(1, config.node("pool_size").getInt(10));
82+
final int queueCapacity = Math.max(poolSize, config.node("queue_capacity").getInt(poolSize * 200));
8283
hikariConfig.setMaximumPoolSize(poolSize);
8384
hikariConfig.setConnectionTimeout(30000);
8485
hikariConfig.setIdleTimeout(600000);
8586
hikariConfig.setMaxLifetime(1800000);
8687
hikariConfig.setLeakDetectionThreshold(2000);
8788

8889
createdDataSource = new HikariDataSource(hikariConfig);
89-
createdExecutor = Executors.newFixedThreadPool(poolSize);
90+
createdExecutor = BoundedExecutorFactory.create("dataprovider-mysql", poolSize, queueCapacity);
9091

9192
try (var connection = createdDataSource.getConnection()) {
9293
if (!connection.isValid(2)) {
@@ -100,11 +101,12 @@ public void connect() {
100101
this.schemaManager = new MySQLSchemaManager(dataSource, executor);
101102

102103
logger.info(String.format(
103-
"[MySQLDatabase] Connected successfully to MySQL at %s:%d (database=%s, sslMode=%s)",
104+
"[MySQLDatabase] Connected successfully to MySQL at %s:%d (database=%s, sslMode=%s, queueCapacity=%d)",
104105
host,
105106
port,
106107
databaseName,
107-
normalizedSslMode
108+
normalizedSslMode,
109+
queueCapacity
108110
));
109111
} catch (Exception e) {
110112
if (createdExecutor != null) {

0 commit comments

Comments
 (0)