mirror of
				https://github.com/Qortal/qortal.git
				synced 2025-11-04 14:17:03 +00:00 
			
		
		
		
	Compare commits
	
		
			3 Commits
		
	
	
		
			bootstrap
			...
			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);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user