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
|
// Lists
|
||||||
private String listsPath = "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. */
|
/** Array of NTP server hostnames. */
|
||||||
private String[] ntpServers = new String[] {
|
private String[] ntpServers = new String[] {
|
||||||
"pool.ntp.org",
|
"pool.ntp.org",
|
||||||
@ -481,6 +487,14 @@ public class Settings {
|
|||||||
return this.listsPath;
|
return this.listsPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getChatRateLimitCount() {
|
||||||
|
return this.chatRateLimitCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getChatRateLimitSeconds() {
|
||||||
|
return this.chatRateLimitSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
public String[] getNtpServers() {
|
public String[] getNtpServers() {
|
||||||
return this.ntpServers;
|
return this.ntpServers;
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import java.util.List;
|
|||||||
import org.qortal.account.Account;
|
import org.qortal.account.Account;
|
||||||
import org.qortal.account.PublicKeyAccount;
|
import org.qortal.account.PublicKeyAccount;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
|
import org.qortal.chat.ChatRateLimiter;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.crypto.MemoryPoW;
|
import org.qortal.crypto.MemoryPoW;
|
||||||
import org.qortal.data.transaction.ChatTransactionData;
|
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)
|
if (chatTransactionData.getData().length < 1 || chatTransactionData.getData().length > MAX_DATA_SIZE)
|
||||||
return ValidationResult.INVALID_DATA_LENGTH;
|
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;
|
return ValidationResult.OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,6 +248,7 @@ public abstract class Transaction {
|
|||||||
INCORRECT_NONCE(94),
|
INCORRECT_NONCE(94),
|
||||||
INVALID_TIMESTAMP_SIGNATURE(95),
|
INVALID_TIMESTAMP_SIGNATURE(95),
|
||||||
ADDRESS_IN_BLACKLIST(96),
|
ADDRESS_IN_BLACKLIST(96),
|
||||||
|
ADDRESS_ABOVE_RATE_LIMIT(97),
|
||||||
INVALID_BUT_OK(999),
|
INVALID_BUT_OK(999),
|
||||||
NOT_YET_RELEASED(1000);
|
NOT_YET_RELEASED(1000);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user