/* This code is part of Freenet. It is distributed under the GNU General * Public License, version 2 (or at your option any later version). See * http://www.gnu.org/ for further details of the GPL. */ package plugins.Freetalk; import java.util.ArrayList; import java.util.Arrays; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import plugins.Freetalk.exceptions.InvalidParameterException; import plugins.Freetalk.exceptions.NoSuchIdentityException; import plugins.Freetalk.exceptions.NoSuchMessageException; import com.db4o.ext.ExtObjectContainer; import freenet.keys.FreenetURI; import freenet.support.Base64; /** * A MessageList contains a list of MessageReference objects. * Each MessageReference object contains the CHK URI of the referenced Message, the ID of the message and a single * Board> to which the Message belongs. If a Message belongs to multiple boards, a MessageReference * is stored for each of them. * Constraints: * - A MessageList must map the ID of a contained message to a single URI. * - A MessageList should not contain multiple mapppings of a URI to the same board. This is considered as DoS * - A MessageList should maybe be limited to a maximal amount of messages references. * - There should be a limit to a certain maximal amount of boards a message can be posted to. */ public abstract class MessageList implements Iterable { protected String mID; /* Not final because OwnMessageList.incrementInsertIndex() might need to change it */ protected final FTIdentity mAuthor; protected int mIndex; /* Not final because OwnMessageList.incrementInsertIndex() might need to change it */ /** * Stores the FreenetURI and the Board of a not downloaded message. If a message is posted to multiple boards, a * MessageReference is stored for each board. This is done to allow querying the database for MessageReference * objects which belong to a certain board - which is necessary because we only want to download messages from boards to which the * user is actually subscribed. */ public static class MessageReference { private MessageList mMessageList = null; private final String mMessageID; private final FreenetURI mURI; @SuppressWarnings("unused") private final Board mBoard; private boolean iWasDownloaded = false; public MessageReference(String newMessageID, FreenetURI newURI, Board myBoard) { if(newMessageID == null || newURI == null || (myBoard == null && !(this instanceof OwnMessageList.OwnMessageReference))) throw new IllegalArgumentException(); /* TODO: Be more verbose */ mMessageID = newMessageID; mURI = newURI; mBoard = myBoard; } private transient ExtObjectContainer db; public void initializeTransient(ExtObjectContainer myDB) { db = myDB; } public synchronized void store() { synchronized(db.lock()) { if(db.ext().isStored(this) && !db.ext().isActive(this)) throw new RuntimeException("Trying to store a non-active MessageList object"); db.store(mURI); db.store(this); db.commit(); } } /** * Returns null, implemented only in OwnMessageList.OwnMessageReference. */ public String getMessageID() { return mMessageID; } public FreenetURI getURI() { return mURI; } public Board getBoard() { return mBoard; } public synchronized boolean wasMessageDownloaded() { return iWasDownloaded; } /** * Marks the MessageReference as downloaded and stores the change in the database. */ public synchronized void setMessageWasDownloadedFlag() { assert(iWasDownloaded == false); iWasDownloaded = true; store(); } public MessageList getMessageList() { db.activate(this, 3); return mMessageList; } /** * Called by it's parent MessageList to store the reference to it. Does not call store(). */ protected void setMessageList(MessageList myMessageList) { mMessageList = myMessageList; } } /** * When a message list fetch fails we need to mark the message list as fetched to prevent the failed message list from getting into the * fetch queue over and over again. An attacker could insert many message list which have unparseable XML to fill up everyone's fetch queue * otherwise, this would be a denial of service. * * When marking a message list as fetched even though the fetch failed, we store a MessageListFetchFailedReference so that we can try to * fetch the message list again in the future. For example when the user installs a new version of the plugin we can fetch all messages list * again with failed XML parsing if the new version has fixed a bug in the XML parser. */ public static class MessageListFetchFailedReference { private final MessageList mMessageList; public static enum Reason { Unknown, DataNotFound, ParsingFailed } private final Reason mReason; public MessageListFetchFailedReference(MessageList myMessageList, Reason myReason) { if(myMessageList == null || myReason == null) throw new IllegalArgumentException(); mMessageList = myMessageList; mReason = myReason; } private transient ExtObjectContainer db; public void initializeTransient(ExtObjectContainer myDB) { db = myDB; } /** * Not synchronized because this class only has final members. */ public void store() { db.store(this); } public MessageList getMessageList() { return mMessageList; } public Reason getReason() { return mReason; } } /** * When a message fetch fails we need to mark the message reference as fetched to prevent the failed message from getting into the * fetch queue over and over again. An attacker could insert many messages which have unparseable XML to fill up everyone's fetch queue * otherwise, this would be a denial of service. * * When marking a message as fetched even though the fetch failed, we store a MessageFetchFailedReference so that we can try to * fetch the message again in the future. For example when the user installs a new version of the plugin we can fetch all messages * again with failed XML parsing if the new version has fixed a bug in the XML parser. */ public static class MessageFetchFailedReference { private final MessageReference mMessageReference; public static enum Reason { Unknown, DataNotFound, ParsingFailed } private final Reason mReason; public MessageFetchFailedReference(MessageReference myMessageReference, Reason myReason) { if(myMessageReference == null || myReason == null) throw new IllegalArgumentException(); mMessageReference = myMessageReference; mReason = myReason; } private transient ExtObjectContainer db; public void initializeTransient(ExtObjectContainer myDB) { db = myDB; } /** * Not synchronized because this class only has final members. */ public void store() { db.store(this); } public MessageReference getMessageReference() { return mMessageReference; } public Reason getReason() { return mReason; } } protected final ArrayList mMessages; /** * To accelerate database queries we also store the message count here instead of obtaining it from the ArrayList of messages. */ protected int mMessageCount; /** * * @param identityManager * @param myURI * @param newMessages A list of the messages. If a message is posted to multiple boards, the list should contain one MessageReference object for each board. * @throws InvalidParameterException * @throws NoSuchIdentityException */ public MessageList(FTIdentity myAuthor, FreenetURI myURI, List newMessages) throws InvalidParameterException, NoSuchIdentityException { this(myAuthor, myURI, new ArrayList(newMessages)); if(mMessages.size() < 1) throw new IllegalArgumentException("Trying to construct a message list with no messages."); Hashtable messageURIs = new Hashtable(newMessages.size()); /* FIXME: 1. Limit the amount of MessageReferences. 2. Limit the amount of boards a single message can be posted to by counting * the number of occurrences of a single FreenetURI in the MessageReference list. 3. Ensure that no (FreenetURI, Board) pair is twice * or more in the list, this would be a DoS attempt. See also the constraints list at the top of this file. * - NOTE: Currently, WoTMessageListXML ensures (3) by using a HashSet. */ for(MessageReference ref : mMessages) { ref.setMessageList(this); try { Message.verifyID(mAuthor, ref.getMessageID()); } catch(InvalidParameterException e) { throw new IllegalArgumentException("Trying to create a MessageList which contains a Message with an ID which does not belong to the author of the MessageList"); } FreenetURI previousURI = messageURIs.put(ref.getMessageID(), ref.getURI()); if(previousURI != null && previousURI.equals(ref.getURI()) == false) throw new IllegalArgumentException("Trying to create a MessageList which maps one message ID to multiple URIs"); } } /** * For constructing an empty dummy message list when the download of message list failed. * @param myAuthor * @param myURI */ public MessageList(FTIdentity myAuthor, FreenetURI myURI) { this(myAuthor, myURI, new ArrayList(0)); } /** * General constructor for being used by public constructors. */ protected MessageList(FTIdentity myAuthor, FreenetURI myURI, ArrayList newMessages) { if(myURI == null) throw new IllegalArgumentException("Trying to construct a MessageList with null URI."); mIndex = (int) myURI.getEdition(); if(mIndex < 0) throw new IllegalArgumentException("Trying to construct a message list with invalid index " + mIndex); if(myAuthor == null || Arrays.equals(myAuthor.getRequestURI().getRoutingKey(), myURI.getRoutingKey()) == false) throw new IllegalArgumentException("Trying to construct a message list with a wrong author " + myAuthor); mAuthor = myAuthor; mID = calculateID(); mMessages = newMessages; mMessageCount = mMessages.size(); } protected MessageList(FTOwnIdentity myAuthor, int newIndex) { if(myAuthor == null) throw new IllegalArgumentException("Trying to construct a MessageList with no author"); if(newIndex < 0) throw new IllegalArgumentException("Trying to construct a message list with invalid index " + newIndex); mAuthor = myAuthor; mIndex = newIndex; mID = calculateID(); mMessages = new ArrayList(16); /* TODO: Find a reasonable value */ } protected transient ExtObjectContainer db; protected transient MessageManager mMessageManager; public void initializeTransient(ExtObjectContainer myDB, MessageManager myMessageManager) { db = myDB; mMessageManager = myMessageManager; } public synchronized void store() { /* FIXME: Check for duplicates */ synchronized(db.lock()) { for(MessageReference ref : mMessages) { ref.initializeTransient(db); ref.store(); } db.store(this); db.commit(); } } protected String calculateID() { return calculateID(mAuthor, mIndex); } public static String calculateID(FTIdentity author, int index) { return index + "@" + Base64.encode(author.getRequestURI().getRoutingKey()); } public static String getIDFromURI(FreenetURI uri) { return uri.getEdition() + "@" + Base64.encode(uri.getRoutingKey()); } public String getID() { return mID; } /** * Get the SSK URI of this message list. * @return */ public FreenetURI getURI() { return generateURI(mAuthor.getRequestURI(), mIndex).sskForUSK(); } /** * Get the USK URI of a message list with the given base URI and index. * @param baseURI * @param index * @return */ protected abstract FreenetURI generateURI(FreenetURI baseURI, int index); public FTIdentity getAuthor() { return mAuthor; } public int getIndex() { return mIndex; } /** * You have to synchronize on the MessageList when using this method. */ public Iterator iterator() { return mMessages.iterator(); } public synchronized MessageReference getReference(FreenetURI messageURI) throws NoSuchMessageException { for(MessageReference ref : this) { if(ref.getURI().equals(messageURI)) return ref; } throw new NoSuchMessageException(); } /* public synchronized void markAsDownloaded(FreenetURI uri) { // TODO: Figure out whether MessageLists are usually large enough so that we can gain speed by using a Hashtable instead of ArrayList for(MessageReference ref : mMessages) { if(ref.getURI().equals(uri)) { ref.markAsDownloaded(); return; } } } */ }