forked from Qortal/qortal
Compare commits
3 Commits
master
...
chat-rate-
Author | SHA1 | Date | |
---|---|---|---|
|
88711ae018 | ||
|
b0f963cca7 | ||
|
2f3e10e15a |
187
src/main/java/org/qortal/chat/ChatDuplicateMessageFilter.java
Normal file
187
src/main/java/org/qortal/chat/ChatDuplicateMessageFilter.java
Normal file
@ -0,0 +1,187 @@
|
||||
package org.qortal.chat;
|
||||
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class ChatDuplicateMessageFilter extends Thread {
|
||||
|
||||
public static class SimpleChatMessage {
|
||||
private long timestamp;
|
||||
private String message;
|
||||
|
||||
public SimpleChatMessage(long timestamp, String message) {
|
||||
this.timestamp = timestamp;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == this)
|
||||
return true;
|
||||
|
||||
if (!(other instanceof SimpleChatMessage))
|
||||
return false;
|
||||
|
||||
SimpleChatMessage otherMessage = (SimpleChatMessage) other;
|
||||
|
||||
return Objects.equals(this.getMessage(), otherMessage.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static ChatDuplicateMessageFilter instance;
|
||||
private volatile boolean isStopping = false;
|
||||
|
||||
private static final int numberOfUniqueMessagesToMonitor = 3; // Only hold the last 3 messages in memory
|
||||
private static final long maxMessageAge = 60 * 60 * 1000L; // Forget messages after 1 hour
|
||||
|
||||
// Maintain a short list of recent chat messages for each address, to save having to query the database every time
|
||||
private Map<String, List<SimpleChatMessage>> recentMessages = new ConcurrentHashMap<>();
|
||||
|
||||
public ChatDuplicateMessageFilter() {
|
||||
|
||||
}
|
||||
|
||||
public static synchronized ChatDuplicateMessageFilter getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new ChatDuplicateMessageFilter();
|
||||
instance.start();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Duplicate Chat Message Filter");
|
||||
|
||||
try {
|
||||
while (!isStopping) {
|
||||
Thread.sleep(60000);
|
||||
|
||||
this.cleanup();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Fall-through to exit thread...
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
isStopping = true;
|
||||
this.interrupt();
|
||||
}
|
||||
|
||||
|
||||
public boolean isDuplicateMessage(String address, long timestamp, String message) {
|
||||
boolean isDuplicateMessage;
|
||||
boolean messagesUpdated = false;
|
||||
|
||||
SimpleChatMessage thisMessage = new SimpleChatMessage(timestamp, message);
|
||||
|
||||
// Add message to array for address
|
||||
List<SimpleChatMessage> messages = new ArrayList<>();
|
||||
if (this.recentMessages.containsKey(address)) {
|
||||
messages = this.recentMessages.get(address);
|
||||
}
|
||||
|
||||
// Check for duplicate, and add if unique
|
||||
if (!messages.contains(thisMessage)) {
|
||||
messages.add(thisMessage);
|
||||
this.recentMessages.put(address, messages);
|
||||
messagesUpdated = true;
|
||||
isDuplicateMessage = false;
|
||||
}
|
||||
else {
|
||||
// Can't add message because it already exists
|
||||
isDuplicateMessage = true;
|
||||
}
|
||||
|
||||
// Ensure we're not tracking more messages than intended
|
||||
while (messages.size() > numberOfUniqueMessagesToMonitor) {
|
||||
messages.remove(0);
|
||||
messagesUpdated = true;
|
||||
}
|
||||
|
||||
// Ensure we're not holding on to messages for longer than a defined time period
|
||||
Iterator iterator = messages.iterator();
|
||||
long now = NTP.getTime();
|
||||
while (iterator.hasNext()) {
|
||||
SimpleChatMessage simpleChatMessage = (SimpleChatMessage) iterator.next();
|
||||
if (simpleChatMessage.getTimestamp() < now - maxMessageAge) {
|
||||
// Older than tracked interval
|
||||
iterator.remove();
|
||||
messagesUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (messagesUpdated) {
|
||||
if (messages.size() > 0) {
|
||||
this.recentMessages.put(address, messages);
|
||||
}
|
||||
else {
|
||||
this.recentMessages.remove(address);
|
||||
}
|
||||
}
|
||||
|
||||
return isDuplicateMessage;
|
||||
}
|
||||
|
||||
|
||||
private void cleanup() {
|
||||
|
||||
// Cleanup map of addresses and messages
|
||||
this.deleteOldMessagesForAllAddresses();
|
||||
}
|
||||
|
||||
private void deleteOldMessagesForAddress(String address, long now) {
|
||||
if (address == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.recentMessages.containsKey(address)) {
|
||||
boolean messagesUpdated = false;
|
||||
|
||||
List<SimpleChatMessage> messages = recentMessages.get(address);
|
||||
|
||||
// Ensure we're not holding on to messages for longer than a defined time period
|
||||
Iterator iterator = messages.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
SimpleChatMessage simpleChatMessage = (SimpleChatMessage) iterator.next();
|
||||
if (simpleChatMessage.getTimestamp() < now - maxMessageAge) {
|
||||
// Older than tracked interval
|
||||
iterator.remove();
|
||||
messagesUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update messages for address
|
||||
if (messagesUpdated) {
|
||||
if (messages.size() > 0) {
|
||||
this.recentMessages.put(address, messages);
|
||||
}
|
||||
else {
|
||||
this.recentMessages.remove(address);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteOldMessagesForAllAddresses() {
|
||||
long now = NTP.getTime();
|
||||
for (Map.Entry<String, List<SimpleChatMessage>> entry : this.recentMessages.entrySet()) {
|
||||
this.deleteOldMessagesForAddress(entry.getKey(), now);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
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,8 @@ import java.util.List;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.chat.ChatDuplicateMessageFilter;
|
||||
import org.qortal.chat.ChatRateLimiter;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.crypto.MemoryPoW;
|
||||
import org.qortal.data.transaction.ChatTransactionData;
|
||||
@ -18,6 +20,7 @@ import org.qortal.repository.Repository;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.ChatTransactionTransformer;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
public class ChatTransaction extends Transaction {
|
||||
|
||||
@ -159,6 +162,20 @@ 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;
|
||||
|
||||
// Check for duplicate messages (unencrypted text messages only)
|
||||
if (!chatTransactionData.getIsEncrypted() && chatTransactionData.getIsText()) {
|
||||
ChatDuplicateMessageFilter duplicateFilter = ChatDuplicateMessageFilter.getInstance();
|
||||
String message58 = Base58.encode(chatTransactionData.getData());
|
||||
if (duplicateFilter.isDuplicateMessage(chatTransactionData.getSender(), chatTransactionData.getTimestamp(), message58))
|
||||
return ValidationResult.DUPLICATE_MESSAGE;
|
||||
}
|
||||
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
|
@ -248,6 +248,8 @@ public abstract class Transaction {
|
||||
INCORRECT_NONCE(94),
|
||||
INVALID_TIMESTAMP_SIGNATURE(95),
|
||||
ADDRESS_IN_BLACKLIST(96),
|
||||
ADDRESS_ABOVE_RATE_LIMIT(97),
|
||||
DUPLICATE_MESSAGE(98),
|
||||
INVALID_BUT_OK(999),
|
||||
NOT_YET_RELEASED(1000);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user