Added chat rate limiter

This can be controlled by two settings:

"chatRateLimitSeconds" - the monitored time range, in seconds. Default: 5 minutes.
"chatRateLimitCount" - the maximum number of messages per address within the above time range. Default: 25.

Exact defaults still TBC. Also, we may decide this is a bad idea altogether, so I've put it in its own branch.
This commit is contained in:
CalDescent 2021-08-07 13:56:32 +01:00
parent cd7adc997b
commit 2f3e10e15a
4 changed files with 174 additions and 0 deletions

View File

@ -0,0 +1,152 @@
package org.qortal.chat;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.settings.Settings;
import org.qortal.utils.NTP;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class ChatRateLimiter extends Thread {
private static ChatRateLimiter instance;
private volatile boolean isStopping = false;
// Maintain a list of recent chat timestamps for each address, to save having to query the database every time
private Map<String, List<Long>> recentMessages = new ConcurrentHashMap<String, List<Long>>();
public ChatRateLimiter() {
}
public static synchronized ChatRateLimiter getInstance() {
if (instance == null) {
instance = new ChatRateLimiter();
instance.start();
}
return instance;
}
@Override
public void run() {
Thread.currentThread().setName("Chat Rate Limiter");
try {
while (!isStopping) {
Thread.sleep(60000);
this.cleanup();
}
} catch (InterruptedException e) {
// Fall-through to exit thread...
}
}
public void shutdown() {
isStopping = true;
this.interrupt();
}
public void addMessage(String address, long timestamp) {
// Add timestamp to array for address
List<Long> timestamps = new ArrayList<Long>();
if (this.recentMessages.containsKey(address)) {
timestamps = this.recentMessages.get(address);
}
if (!timestamps.contains(timestamp)) {
timestamps.add(timestamp);
}
this.recentMessages.put(address, timestamps);
}
public boolean isAddressAboveRateLimit(String address) {
int chatRateLimitCount = Settings.getInstance().getChatRateLimitCount();
long chatRateLimitMilliseconds = Settings.getInstance().getChatRateLimitSeconds() * 1000L;
long now = NTP.getTime();
if (this.recentMessages.containsKey(address)) {
int messageCount = 0;
boolean timestampsUpdated = false;
List<Long> timestamps = this.recentMessages.get(address);
Iterator iterator = timestamps.iterator();
while (iterator.hasNext()) {
Long timestamp = (Long) iterator.next();
if (timestamp >= now - chatRateLimitMilliseconds) {
// Message within tracked range
messageCount++;
}
else {
// Older than tracked range - delete to reduce memory consumption
iterator.remove();
timestampsUpdated = true;
}
}
// Update timestamps for address
if (timestampsUpdated) {
if (timestamps.size() > 0) {
this.recentMessages.put(address, timestamps);
}
else {
this.recentMessages.remove(address);
}
}
if (messageCount >= chatRateLimitCount) {
// Rate limit has been hit
return true;
}
}
return false;
}
private void cleanup() {
// Cleanup map of addresses and timestamps
this.deleteOldTimestampsForAllAddresses();
}
private void deleteOldTimestampsForAddress(String address) {
if (address == null) {
return;
}
long chatRateLimitMilliseconds = Settings.getInstance().getChatRateLimitSeconds() * 1000L;
long now = NTP.getTime();
if (this.recentMessages.containsKey(address)) {
boolean timestampsUpdated = false;
List<Long> timestamps = recentMessages.get(address);
Iterator iterator = timestamps.iterator();
while (iterator.hasNext()) {
Long timestamp = (Long) iterator.next();
if (timestamp < now - chatRateLimitMilliseconds) {
// Older than tracked interval
iterator.remove();
timestampsUpdated = true;
}
}
// Update timestamps for address
if (timestampsUpdated) {
if (timestamps.size() > 0) {
this.recentMessages.put(address, timestamps);
} else {
this.recentMessages.remove(address);
}
}
}
}
private void deleteOldTimestampsForAllAddresses() {
for (Map.Entry<String, List<Long>> entry : this.recentMessages.entrySet()) {
this.deleteOldTimestampsForAddress(entry.getKey());
}
}
}

View File

@ -164,6 +164,12 @@ public class Settings {
// Lists
private String listsPath = "lists";
// Chat rate limit
/** Limit to 20 messages per address... */
private int chatRateLimitCount = 25;
/** ...per 5 minutes of time that passes */
private int chatRateLimitSeconds = 5 * 60;
/** Array of NTP server hostnames. */
private String[] ntpServers = new String[] {
"pool.ntp.org",
@ -481,6 +487,14 @@ public class Settings {
return this.listsPath;
}
public int getChatRateLimitCount() {
return this.chatRateLimitCount;
}
public int getChatRateLimitSeconds() {
return this.chatRateLimitSeconds;
}
public String[] getNtpServers() {
return this.ntpServers;
}

View File

@ -6,6 +6,7 @@ import java.util.List;
import org.qortal.account.Account;
import org.qortal.account.PublicKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.chat.ChatRateLimiter;
import org.qortal.crypto.Crypto;
import org.qortal.crypto.MemoryPoW;
import org.qortal.data.transaction.ChatTransactionData;
@ -159,6 +160,12 @@ public class ChatTransaction extends Transaction {
if (chatTransactionData.getData().length < 1 || chatTransactionData.getData().length > MAX_DATA_SIZE)
return ValidationResult.INVALID_DATA_LENGTH;
// Check rate limit
ChatRateLimiter rateLimiter = ChatRateLimiter.getInstance();
rateLimiter.addMessage(chatTransactionData.getSender(), chatTransactionData.getTimestamp());
if (rateLimiter.isAddressAboveRateLimit(chatTransactionData.getSender()))
return ValidationResult.ADDRESS_ABOVE_RATE_LIMIT;
return ValidationResult.OK;
}

View File

@ -248,6 +248,7 @@ public abstract class Transaction {
INCORRECT_NONCE(94),
INVALID_TIMESTAMP_SIGNATURE(95),
ADDRESS_IN_BLACKLIST(96),
ADDRESS_ABOVE_RATE_LIMIT(97),
INVALID_BUT_OK(999),
NOT_YET_RELEASED(1000);