forked from Qortal/qortal
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:
parent
cd7adc997b
commit
2f3e10e15a
152
src/main/java/org/qortal/chat/ChatRateLimiter.java
Normal file
152
src/main/java/org/qortal/chat/ChatRateLimiter.java
Normal 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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user