Java tutorial
/* * Channel.java * This file is part of Freemail * Copyright (C) 2006,2007,2008 Dave Baker * Copyright (C) 2007 Alexander Lehmann * Copyright (C) 2009 Martin Nyhus * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package org.freenetproject.freemail.transport; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.net.MalformedURLException; import java.util.Iterator; import java.util.Locale; import java.util.Map.Entry; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import org.archive.util.Base32; import org.bouncycastle.crypto.AsymmetricBlockCipher; import org.bouncycastle.crypto.InvalidCipherTextException; import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.crypto.engines.AESEngine; import org.bouncycastle.crypto.engines.RSAEngine; import org.bouncycastle.crypto.modes.CBCBlockCipher; import org.bouncycastle.crypto.paddings.PKCS7Padding; import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.crypto.params.ParametersWithIV; import org.bouncycastle.crypto.params.RSAKeyParameters; import org.freenetproject.freemail.AccountManager; import org.freenetproject.freemail.Freemail; import org.freenetproject.freemail.Freemail.TaskType; import org.freenetproject.freemail.FreemailAccount; import org.freenetproject.freemail.FreenetURI; import org.freenetproject.freemail.SlotManager; import org.freenetproject.freemail.SlotSaveCallback; import org.freenetproject.freemail.fcp.ConnectionTerminatedException; import org.freenetproject.freemail.fcp.FCPBadFileException; import org.freenetproject.freemail.fcp.FCPException; import org.freenetproject.freemail.fcp.FCPFetchException; import org.freenetproject.freemail.fcp.FCPPutFailedException; import org.freenetproject.freemail.fcp.HighLevelFCPClient; import org.freenetproject.freemail.fcp.SSKKeyPair; import org.freenetproject.freemail.utils.DateStringFactory; import org.freenetproject.freemail.utils.Logger; import org.freenetproject.freemail.utils.PropsFile; import org.freenetproject.freemail.utils.Timer; import org.freenetproject.freemail.wot.Identity; import org.freenetproject.freemail.wot.WoTConnection; import org.freenetproject.freemail.wot.WoTProperties; import freenet.keys.InsertableClientSSK; import freenet.pluginmanager.PluginNotFoundException; import freenet.support.api.Bucket; import freenet.support.io.ArrayBucket; import freenet.support.io.BucketTools; import freenet.support.io.Closer; //FIXME: The message id gives away how many messages has been sent over the channel. // Could it be replaced by a different solution that gives away less information? class Channel { private static final String CHANNEL_PROPS_NAME = "props"; private static final int POLL_AHEAD = 6; private static final String ACK_LOG = "acklog"; private static final long MAX_ACK_DELAY = 12 * 60 * 60 * 1000; //12 hours /** * The amount of time before the channel times out, in milliseconds. If the channel is created * at t=0, then messages won't be queued after t=CHANNEL_TIMEOUT, and the fetcher will stop * when t=2*CHANNEL_TIMEOUT. This should provide enough delay for the recipient to fetch all * the messages. */ private static final long CHANNEL_TIMEOUT = 7 * 24 * 60 * 60 * 1000; //1 week /** The amount of time to wait before retrying after a transient failure. */ private static final long TASK_RETRY_DELAY = 5 * 60 * 1000; //5 minutes //The keys used in the props file private static class PropsKeys { private static final String PRIVATE_KEY = "privateKey"; private static final String PUBLIC_KEY = "publicKey"; private static final String FETCH_SLOT = "fetchSlot"; private static final String SEND_SLOT = "sendSlot"; private static final String SENDER_STATE = "sender-state"; private static final String RECIPIENT_STATE = "recipient-state"; private static final String RTS_SENT_AT = "rts-sent-at"; private static final String SEND_CODE = "sendCode"; private static final String FETCH_CODE = "fetchCode"; private static final String REMOTE_ID = "remoteID"; private static final String TIMEOUT = "timeout"; private static final String MSG_SLOT = ".slot"; } private static class RTSKeys { private static final String MAILSITE = "mailsite"; private static final String TO = "to"; private static final String CHANNEL = "channel"; private static final String INITIATOR_SLOT = "initiatorSlot"; private static final String RESPONDER_SLOT = "responderSlot"; private static final String TIMEOUT = "timeout"; } private final File channelDir; private final PropsFile channelProps; private final ScheduledExecutorService executor; private final HighLevelFCPClient fcpClient; private final Freemail freemail; private final FreemailAccount account; private final Fetcher fetcher = new Fetcher(); private final RTSSender rtsSender = new RTSSender(); private final AtomicReference<ChannelEventCallback> channelEventCallback = new AtomicReference<ChannelEventCallback>(); private final MessageLog ackLog; Channel(File channelDir, ScheduledExecutorService executor, HighLevelFCPClient fcpClient, Freemail freemail, FreemailAccount account, String remoteId) throws ChannelTimedOutException { if (executor == null) throw new NullPointerException(); this.executor = executor; this.fcpClient = fcpClient; this.account = account; if (freemail == null) throw new NullPointerException(); this.freemail = freemail; assert channelDir.isDirectory(); this.channelDir = channelDir; ackLog = new MessageLog(new File(channelDir, ACK_LOG)); File channelPropsFile = new File(channelDir, CHANNEL_PROPS_NAME); if (!channelPropsFile.exists()) { try { if (!channelPropsFile.createNewFile()) { Logger.error(this, "Could not create new props file in " + channelDir); } } catch (IOException e) { Logger.error(this, "Could not create new props file in " + channelDir); } } channelProps = PropsFile.createPropsFile(channelPropsFile); //Check if the channel has timed out synchronized (channelProps) { String rawTimeout = channelProps.get(PropsKeys.TIMEOUT); if (rawTimeout != null) { try { long timeout = Long.parseLong(rawTimeout); long left = timeout + CHANNEL_TIMEOUT - System.currentTimeMillis(); Logger.debug(this, "Time left until timeout: " + left + "ms (read only in " + (left - CHANNEL_TIMEOUT) + "ms)"); if (left < 0) { Logger.debug(this, "Channel has timed out"); throw new ChannelTimedOutException(); } } catch (NumberFormatException e) { Logger.error(this, "Illegal value in " + PropsKeys.TIMEOUT + " field, assuming timed out: " + rawTimeout); throw new ChannelTimedOutException(); } } } //Set remote id if given if (remoteId != null) { synchronized (channelProps) { String prev = channelProps.get(PropsKeys.REMOTE_ID); if ((prev != null) && (!remoteId.equals(prev))) { /* Since the remote id should only be set when the * channel is created this shouldn't happen. */ Logger.error(this, "Changing remote id"); Logger.debug(this, "Changing remote id from " + prev + " to " + remoteId); assert (false) : "Changing remote id from " + prev + " to " + remoteId; } channelProps.put(PropsKeys.REMOTE_ID, remoteId); } } else { //If not, make sure it is in the config file synchronized (channelProps) { String prev = channelProps.get(PropsKeys.REMOTE_ID); if (prev == null) { Logger.error(this, "Channel is broken because remote id is missing (" + channelDir + ")"); throw new IllegalStateException("Remote id missing"); } } } } void processRTS(PropsFile rtsProps) { Logger.debug(this, "Processing RTS"); synchronized (channelProps) { //Because of the way InsertableClientSSK works we need to add a document name (the part //after the final /) to the key before it is passed to FreenetURI. This must be removed //again when we store the keys to the props file String privateKey = ""; String publicKey = ""; try { final String documentName = "documentName"; freenet.keys.FreenetURI privateURI = new freenet.keys.FreenetURI(rtsProps.get(RTSKeys.CHANNEL)); privateURI = privateURI.setDocName(documentName); InsertableClientSSK insertableKey = InsertableClientSSK.create(privateURI); privateKey = insertableKey.getInsertURI().setDocName("").toString(); publicKey = insertableKey.getURI().setDocName("").toString(); } catch (MalformedURLException e) { Logger.debug(this, "RTS contained malformed private key: " + rtsProps.get(RTSKeys.CHANNEL)); return; } if (channelProps.get(PropsKeys.RECIPIENT_STATE) != null) { Logger.debug(this, "Skipping RTS processing because recipient state isn't null"); return; } if (channelProps.get(PropsKeys.PRIVATE_KEY) == null) { channelProps.put(PropsKeys.PRIVATE_KEY, privateKey); channelProps.put(PropsKeys.PUBLIC_KEY, publicKey); } channelProps.put(PropsKeys.FETCH_SLOT, rtsProps.get(RTSKeys.INITIATOR_SLOT)); channelProps.put(PropsKeys.FETCH_CODE, "i"); if (channelProps.get(PropsKeys.SEND_CODE) == null) { channelProps.put(PropsKeys.SEND_CODE, "r"); } if (channelProps.get(PropsKeys.SEND_SLOT) == null) { channelProps.put(PropsKeys.SEND_SLOT, rtsProps.get(RTSKeys.RESPONDER_SLOT)); } channelProps.put(PropsKeys.TIMEOUT, rtsProps.get(RTSKeys.TIMEOUT)); channelProps.put(PropsKeys.RECIPIENT_STATE, "rts-received"); } //Queue the CTS insert try { executor.execute(new CTSInserter()); } catch (RejectedExecutionException e) { Logger.debug(this, "Caugth RejectedExecutionException while scheduling CTSInserter"); } startFetcher(); } void setCallback(ChannelEventCallback callback) { //At the moment we only need to set the callback after creating the //channel, and the rest of the code hasn't been checked to make sure //it can handle a change, so be strict about it. if (!channelEventCallback.compareAndSet(null, callback)) { throw new IllegalStateException("Callback has already been set"); } } public static boolean deleteChannel(File channelDir) { File channelPropsFile = new File(channelDir, CHANNEL_PROPS_NAME); channelPropsFile.delete(); File ackLog = new File(channelDir, ACK_LOG); ackLog.delete(); return channelDir.delete(); } private class CTSInserter implements Runnable { @Override public void run() { Logger.debug(this, "CTSInserter running (" + this + ")"); //Build the header of the inserted message Bucket bucket; try { bucket = new ArrayBucket("messagetype=cts\r\n\r\n".getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { //JVMs are required to support UTF-8, so we can assume it is always available throw new AssertionError("JVM doesn't support UTF-8 charset"); } boolean inserted; try { inserted = insertMessage(bucket, "cts"); } catch (IOException e) { //The getInputStream() method of ArrayBucket doesn't throw throw new AssertionError(); } catch (InterruptedException e) { Logger.debug(this, "CTSInserter interrupted, quitting"); return; } if (inserted) { synchronized (channelProps) { channelProps.put(PropsKeys.RECIPIENT_STATE, "cts-sent"); } } else { try { executor.schedule(this, TASK_RETRY_DELAY, TimeUnit.MILLISECONDS); } catch (RejectedExecutionException e) { Logger.debug(this, "Caugth RejectedExecutionException while scheduling CTSInserter"); } } } } void startTasks() { startFetcher(); startRTSSender(); //Start insert of acks that were written to disk but not inserted try { synchronized (ackLog) { Iterator<Entry<Long, String>> it = ackLog.iterator(); while (it.hasNext()) { Entry<Long, String> entry = it.next(); long insertAfter = 0; if (entry.getValue() != null) { try { insertAfter = Long.parseLong(entry.getValue()); } catch (NumberFormatException e) { //Assume no delay insertAfter = 0; } } ScheduledExecutorService senderExecutor = freemail.getExecutor(TaskType.SENDER); senderExecutor.execute(new AckInserter(entry.getKey(), insertAfter)); } } } catch (IOException e) { Logger.error(this, "Caugth IOException while checking acklog: " + e.getMessage(), e); } catch (RejectedExecutionException e) { // Catch it here instead of inside the loop since there is no point // in trying to schedule the rest Logger.debug(this, "Caugth RejectedExecutionException while scheduling AckInserter"); } //Start the CTS sender if needed synchronized (channelProps) { String recipientState = channelProps.get(PropsKeys.RECIPIENT_STATE); if ("rts-received".equals(recipientState)) { try { executor.execute(new CTSInserter()); } catch (RejectedExecutionException e) { Logger.debug(this, "Caugth RejectedExecutionException while scheduling CTSInserter"); } } } } private void startFetcher() { //Start fetcher if possible String fetchSlot; String fetchCode; String publicKey; synchronized (channelProps) { fetchSlot = channelProps.get(PropsKeys.FETCH_SLOT); fetchCode = channelProps.get(PropsKeys.FETCH_CODE); publicKey = channelProps.get(PropsKeys.PUBLIC_KEY); } if ((fetchSlot != null) && (fetchCode != null) && (publicKey != null)) { fetcher.execute(); } } private void startRTSSender() { String state; synchronized (channelProps) { state = channelProps.get(PropsKeys.SENDER_STATE); } if ((state != null) && state.equals("cts-received")) { return; } rtsSender.execute(); } /** * Sends the message that is read from {@code message}, returning {@code true} if the message * was inserted. The caller is responsible for freeing {@code message}. * @param message the data to be sent * @param messageId the message id that has been assigned to this message * @return {@code true} if the message was sent, {@code false} otherwise * @throws ChannelTimedOutException if the channel has timed out and can't be used for sending * messages * @throws IOException if any operations on {@code message} throws IOException * @throws InterruptedException if the current thread was interrupted while sending the message * @throws NullPointerException if {@code message} is {@code null} */ boolean sendMessage(Bucket message, long messageId) throws ChannelTimedOutException, IOException, InterruptedException { if (message == null) throw new NullPointerException("Parameter message was null"); synchronized (channelProps) { String rawTimeout = channelProps.get(PropsKeys.TIMEOUT); if (rawTimeout != null) { long timeout; try { timeout = Long.parseLong(rawTimeout); } catch (NumberFormatException e) { timeout = 0; } if (timeout < System.currentTimeMillis()) { throw new ChannelTimedOutException(); } } } //Build the header of the inserted message String header = "messagetype=message\r\n" + "id=" + messageId + "\r\n" + "\r\n"; Bucket messageHeader = new ArrayBucket(header.getBytes("UTF-8")); //Now combine them in a single bucket ArrayBucket fullMessage = new ArrayBucket(); OutputStream messageOutputStream = null; try { messageOutputStream = fullMessage.getOutputStream(); BucketTools.copyTo(messageHeader, messageOutputStream, -1); BucketTools.copyTo(message, messageOutputStream, -1); } finally { Closer.close(messageOutputStream); } return insertMessage(fullMessage, "msg" + messageId); } /** * Inserts the given message to the next available slot, returning {@code true} if the message * was inserted, {@code false} otherwise. * @param message the message that should be inserted * @return {@code true} if the message was inserted, {@code false} otherwise * @throws IOException if the getInputStream() method of message throws IOException * @throws InterruptedException if the current thread was interrupted while inserting the message */ private boolean insertMessage(Bucket message, String prefix) throws IOException, InterruptedException { String privateKey; String sendCode; synchronized (channelProps) { privateKey = channelProps.get(PropsKeys.PRIVATE_KEY); sendCode = channelProps.get(PropsKeys.SEND_CODE); if (privateKey == null) { /* Most likely because we tried sending the message before sending the RTS */ Logger.minor(this, "Can't insert, missing private key"); return false; } if (sendCode == null) { /* If we have the private key, but not the send code something * is wrong since sending/receiving an RTS stores these atomically */ /* FIXME: Perhaps set the timeout to 0 so the channel will be deleted? */ Logger.error(this, "Can't insert, missing send code but have private key"); return false; } } while (true) { /* First we get the slot for this message if one has been assigned */ String sendSlot; synchronized (channelProps) { /* If a slot has been assigned, use it */ sendSlot = channelProps.get(prefix + PropsKeys.MSG_SLOT); if (sendSlot == null) { /* If not, assign the next free slot */ sendSlot = channelProps.get(PropsKeys.SEND_SLOT); String nextSlot = calculateNextSlot(sendSlot); channelProps.put(PropsKeys.SEND_SLOT, nextSlot); channelProps.put(prefix + PropsKeys.MSG_SLOT, sendSlot); Logger.debug(this, "Assigned slot " + sendSlot + " to message " + prefix); } } String insertKey = privateKey + sendCode + "-" + sendSlot; InputStream messageStream = null; try { messageStream = message.getInputStream(); Logger.minor(this, "Inserting data"); Logger.debug(this, "Insert key is " + insertKey); FCPPutFailedException fcpMessage; try { Timer messageInsert = Timer.start(); fcpMessage = fcpClient.put(messageStream, insertKey); messageInsert.log(this, 1, TimeUnit.HOURS, "Time spent inserting message"); } catch (FCPBadFileException e) { Logger.error(this, "Caugth FCPBadFileException while inserting message", e); return false; } catch (ConnectionTerminatedException e) { /* Expected if Freemail is shutting down */ Logger.debug(this, "Caugth " + e); return false; } catch (FCPException e) { Logger.error(this, "Unexpected error while inserting data: " + e.getMessage()); return false; } if (fcpMessage == null) { Logger.minor(this, "Insert successful"); synchronized (channelProps) { if (!channelProps.remove(prefix + PropsKeys.MSG_SLOT)) { Logger.error(this, "Couldn't remove slot, will try again later"); /* * The insert succeeded, but we can't leave the slot in the props file * since that would break the forward secrecy of the slot system. By * returning false we will try again later (using the same slot) and * hopefully we can delete it then. */ return false; } } return true; } if (fcpMessage.errorcode == FCPPutFailedException.COLLISION) { synchronized (channelProps) { sendSlot = channelProps.get(PropsKeys.SEND_SLOT); String nextSlot = calculateNextSlot(sendSlot); channelProps.put(PropsKeys.SEND_SLOT, nextSlot); channelProps.put(prefix + PropsKeys.MSG_SLOT, sendSlot); } Logger.debug(this, "Insert collided, assigned new slot " + sendSlot + " to message " + prefix); } /* TODO: Log at a higher level for more serious errors */ Logger.minor(this, "Insert failed, error code " + fcpMessage.errorcode); return false; } finally { Closer.close(messageStream); } } } @Override public String toString() { return "Channel [" + channelDir + "]"; } String getRemoteIdentity() { synchronized (channelProps) { return channelProps.get(PropsKeys.REMOTE_ID); } } String getPrivateKey() { synchronized (channelProps) { return channelProps.get(PropsKeys.PRIVATE_KEY); } } boolean canSendMessages() { synchronized (channelProps) { String rawTimeout = channelProps.get(PropsKeys.TIMEOUT); if (rawTimeout == null) { //RTS hasn't been sent yet return true; } long timeout; try { timeout = Long.parseLong(rawTimeout); } catch (NumberFormatException e) { Logger.error(this, "Returning false from canSendMessages(), parse error: " + rawTimeout); return false; } return timeout >= System.currentTimeMillis(); } } private String calculateNextSlot(String slot) { byte[] buf = Base32.decode(slot); SHA256Digest sha256 = new SHA256Digest(); sha256.update(buf, 0, buf.length); sha256.doFinal(buf, 0); return Base32.encode(buf); } private String generateRandomSlot() { SHA256Digest sha256 = new SHA256Digest(); byte[] buf = new byte[sha256.getDigestSize()]; Freemail.getRNG().nextBytes(buf); return Base32.encode(buf); } private class Fetcher implements Runnable { private final AtomicLong lastRun = new AtomicLong(); @Override public synchronized void run() { long curTime = System.currentTimeMillis(); long last = lastRun.getAndSet(curTime); if (last != 0) { Logger.debug(this, "Fetcher running (" + this + "), last ran " + (curTime - last) + "ms ago"); } else { Logger.debug(this, "Fetcher running (" + this + ")"); } try { realRun(); } catch (RuntimeException e) { Logger.error(this, "Caugth RuntimeException while running fetcher", e); throw e; } catch (Error e) { Logger.error(this, "Caugth Error while running fetcher", e); throw e; } catch (InterruptedException e) { Logger.debug(this, "Fetcher interrupted, quitting"); return; } } private void realRun() throws InterruptedException { synchronized (channelProps) { String rawTimeout = channelProps.get(PropsKeys.TIMEOUT); long timeout; try { timeout = Long.parseLong(rawTimeout); } catch (NumberFormatException e) { //Assume we haven't timed out timeout = Long.MAX_VALUE; } //Check if we've timed out. The extra time is added because we want to stop fetching //later than we stop sending. See JavaDoc for CHANNEL_TIMEOUT for details if (timeout < (System.currentTimeMillis() - CHANNEL_TIMEOUT)) { Logger.debug(this, "Channel has timed out, won't fetch"); return; } } String slots; synchronized (channelProps) { slots = channelProps.get(PropsKeys.FETCH_SLOT); } if (slots == null) { Logger.error(this, "Channel " + channelDir.getName() + " is corrupt - account file has no '" + PropsKeys.FETCH_SLOT + "' entry!"); //TODO: Either delete the channel or resend the RTS return; } HashSlotManager slotManager = new HashSlotManager( new ChannelSlotSaveImpl(channelProps, PropsKeys.FETCH_SLOT), null, slots); slotManager.setPollAhead(POLL_AHEAD); String basekey; synchronized (channelProps) { basekey = channelProps.get(PropsKeys.PUBLIC_KEY); } if (basekey == null) { Logger.error(this, "Channel " + channelDir.getName() + " is corrupt - account file has no '" + PropsKeys.PUBLIC_KEY + "' entry!"); //TODO: Either delete the channel or resend the RTS return; } String fetchCode; synchronized (channelProps) { fetchCode = channelProps.get(PropsKeys.FETCH_CODE); } if (fetchCode == null) { Logger.error(this, "Channel " + channelDir.getName() + " is corrupt - account file has no '" + PropsKeys.FETCH_CODE + "' entry!"); //TODO: Either delete the channel or resend the RTS return; } basekey += fetchCode + "-"; String slot; while ((slot = slotManager.getNextSlot()) != null) { String key = basekey + slot; Logger.debug(this, "Attempting to fetch mail on key " + key); File result; try { result = fcpClient.fetch(key); } catch (ConnectionTerminatedException e) { Logger.debug(this, "Connection terminated"); return; } catch (FCPFetchException e) { if (e.getCode() == FCPFetchException.INVALID_URI) { //Could be a local bug or we could have gotten a bad key in the RTS //TODO: This won't fix itself, so make sure the user notices Logger.error(this, "Fetch failed because the URI was invalid"); return; } if (e.isFatal()) { Logger.normal(this, "Fatal fetch failure, marking slot as used"); slotManager.slotUsed(); } Logger.minor(this, "No mail in slot (fetch returned " + e.getMessage() + ")"); continue; } catch (FCPException e) { Logger.error(this, "Unexpected error while trying to fetch message: " + e.getMessage()); return; } Logger.debug(this, "Fetch successful"); PropsFile messageProps = PropsFile.createPropsFile(result, true); String messageType = messageProps.get("messagetype"); if (messageType == null) { Logger.error(this, "Got message without messagetype, discarding"); slotManager.slotUsed(); result.delete(); continue; } if (messageType.equals("message")) { if (handleMessage(result)) { slotManager.slotUsed(); } } else if (messageType.equals("cts")) { Logger.minor(this, "Successfully received CTS"); boolean success; synchronized (channelProps) { success = channelProps.put(PropsKeys.SENDER_STATE, "cts-received"); } if (success) { slotManager.slotUsed(); } } else if (messageType.equals("ack")) { if (handleAck(result)) { slotManager.slotUsed(); } } else { Logger.error(this, "Got message of unknown type: " + messageType); slotManager.slotUsed(); } if (!result.delete()) { Logger.error(this, "Deletion of " + result + " failed"); } } //Reschedule schedule(TASK_RETRY_DELAY, TimeUnit.MILLISECONDS); } public void execute() { Logger.debug(this, "Scheduling Fetcher for execution"); try { executor.execute(fetcher); } catch (RejectedExecutionException e) { Logger.debug(this, "Caugth RejectedExecutionException while scheduling Fetcher"); } } public void schedule(long delay, TimeUnit unit) { Logger.debug(this, "Scheduling Fetcher for execution in " + delay + " " + unit.toString().toLowerCase(Locale.ROOT)); try { executor.schedule(fetcher, delay, unit); } catch (RejectedExecutionException e) { Logger.debug(this, "Caugth RejectedExecutionException while scheduling Fetcher"); } } @Override public String toString() { return "Fetcher [" + channelDir + "]"; } } private class RTSSender implements Runnable { @Override public synchronized void run() { Logger.debug(this, "RTSSender running (" + this + ")"); try { realRun(); } catch (RuntimeException e) { Logger.error(this, "Caugth RuntimeException while running RTSSender", e); throw e; } catch (Error e) { Logger.error(this, "Caugth Error while running RTSSender", e); throw e; } catch (InterruptedException e) { Logger.debug(this, "RTSSender interrupted, quitting"); } } private void realRun() throws InterruptedException { //Check when the RTS should be sent long sendRtsIn = sendRTSIn(); if (sendRtsIn < 0) { return; } if (sendRtsIn > 0) { Logger.debug(this, "Rescheduling RTSSender in " + sendRtsIn + " ms when the RTS is due to be inserted"); schedule(sendRtsIn, TimeUnit.MILLISECONDS); return; } //Get or generate RTS values String privateKey; String publicKey; String initiatorSlot; String responderSlot; long timeout; synchronized (channelProps) { privateKey = channelProps.get(PropsKeys.PRIVATE_KEY); publicKey = channelProps.get(PropsKeys.PUBLIC_KEY); initiatorSlot = channelProps.get(PropsKeys.SEND_SLOT); responderSlot = channelProps.get(PropsKeys.FETCH_SLOT); if ((privateKey == null) || (publicKey == null)) { SSKKeyPair keyPair; try { Logger.debug(this, "Making new key pair"); keyPair = fcpClient.makeSSK(); } catch (ConnectionTerminatedException e) { Logger.debug(this, "FCP connection has been terminated"); return; } privateKey = keyPair.privkey; publicKey = keyPair.pubkey; } if (initiatorSlot == null) { initiatorSlot = generateRandomSlot(); } if (responderSlot == null) { responderSlot = generateRandomSlot(); } String rawTimeout = channelProps.get(PropsKeys.TIMEOUT); try { timeout = Long.parseLong(rawTimeout); } catch (NumberFormatException e) { if (rawTimeout != null) { Logger.error(this, "Illegal value in timeout field: " + rawTimeout); } timeout = System.currentTimeMillis() + CHANNEL_TIMEOUT; } channelProps.put(PropsKeys.PUBLIC_KEY, publicKey); channelProps.put(PropsKeys.PRIVATE_KEY, privateKey); channelProps.put(PropsKeys.SEND_SLOT, initiatorSlot); channelProps.put(PropsKeys.FETCH_SLOT, responderSlot); channelProps.put(PropsKeys.SEND_CODE, "i"); channelProps.put(PropsKeys.FETCH_CODE, "r"); channelProps.put(PropsKeys.TIMEOUT, "" + timeout); } //Check the timeout. If the channel is already in //read-only mode there is no need to resend the RTS if (timeout < System.currentTimeMillis()) { Logger.debug(this, "Channel in read-only mode, won't resend RTS"); return; } //Get mailsite key from WoT WoTConnection wotConnection = freemail.getWotConnection(); if (wotConnection == null) { //WoT isn't loaded, so try again later Logger.debug(this, "WoT not loaded, trying again in 5 minutes"); schedule(TASK_RETRY_DELAY, TimeUnit.MILLISECONDS); return; } String remoteId; synchronized (channelProps) { remoteId = channelProps.get(PropsKeys.REMOTE_ID); } if (remoteId == null) { /* FIXME: Make sure the channel is deleted, e.g. by setting TIMEOUT to 0 */ Logger.error(this, "Missing remote identity"); return; } String senderId = account.getIdentity(); Logger.debug(this, "Getting identity from WoT"); Identity recipient; try { recipient = wotConnection.getIdentity(remoteId, senderId); } catch (PluginNotFoundException e) { Logger.error(this, "WoT plugin isn't loaded, can't send RTS"); recipient = null; } if (recipient == null) { Logger.debug(this, "Didn't get identity from WoT, trying again in 5 minutes"); schedule(TASK_RETRY_DELAY, TimeUnit.MILLISECONDS); return; } //Get the mailsite edition int mailisteEdition; String edition; try { edition = wotConnection.getProperty(remoteId, WoTProperties.MAILSITE_EDITION); } catch (PluginNotFoundException e1) { edition = null; } if (edition == null) { mailisteEdition = 1; } else { try { mailisteEdition = Integer.parseInt(edition); } catch (NumberFormatException e) { mailisteEdition = 1; } } //Strip the WoT part from the key and add the Freemail path String mailsiteKey = recipient.getRequestURI(); mailsiteKey = mailsiteKey.substring(0, mailsiteKey.indexOf("/")); mailsiteKey = mailsiteKey + "/mailsite/-" + mailisteEdition + "/mailpage"; //Fetch the mailsite File mailsite; try { Logger.debug(this, "Fetching mailsite from " + mailsiteKey); mailsite = fcpClient.fetch(mailsiteKey); } catch (ConnectionTerminatedException e) { Logger.debug(this, "FCP connection has been terminated"); return; } catch (FCPFetchException e) { Logger.debug(this, "Mailsite fetch failed (" + e + "), trying again in 5 minutes"); schedule(TASK_RETRY_DELAY, TimeUnit.MILLISECONDS); return; } catch (FCPException e) { Logger.error(this, "Unexpected error while fetching mailsite: " + e.getMessage()); return; } //Get RTS KSK PropsFile mailsiteProps = PropsFile.createPropsFile(mailsite, false); String rtsKey = mailsiteProps.get("rtsksk"); if (rtsKey == null) { Logger.error(this, "Mailsite is missing RTS KSK"); schedule(1, TimeUnit.HOURS); return; } //Get the senders mailsite key Logger.debug(this, "Getting sender identity from WoT"); Identity senderIdentity; try { senderIdentity = wotConnection.getIdentity(senderId, senderId); } catch (PluginNotFoundException e) { Logger.error(this, "WoT plugin not loaded, can't send RTS"); senderIdentity = null; } if (senderIdentity == null) { Logger.debug(this, "Didn't get identity from WoT, trying again in 5 minutes"); schedule(TASK_RETRY_DELAY, TimeUnit.MILLISECONDS); return; } int senderMailsiteEdition; String senderEdition; try { senderEdition = wotConnection.getProperty(account.getIdentity(), WoTProperties.MAILSITE_EDITION); } catch (PluginNotFoundException e1) { senderEdition = null; } if (edition == null) { senderMailsiteEdition = 1; } else { try { senderMailsiteEdition = Integer.parseInt(senderEdition); } catch (NumberFormatException e) { senderMailsiteEdition = 1; } } String senderMailsiteKey = senderIdentity.getRequestURI(); senderMailsiteKey = senderMailsiteKey.substring(0, senderMailsiteKey.indexOf("/")); senderMailsiteKey = senderMailsiteKey + "/mailsite/-" + senderMailsiteEdition + "/mailpage"; //Now build the RTS byte[] rtsMessageBytes = buildRTSMessage(senderMailsiteKey, recipient.getIdentityID(), privateKey, initiatorSlot, responderSlot, timeout); if (rtsMessageBytes == null) { return; } //Sign the message byte[] signedMessage = signRtsMessage(rtsMessageBytes); if (signedMessage == null) { return; } //Encrypt the message using the recipients public key String keyModulus = mailsiteProps.get("asymkey.modulus"); if (keyModulus == null) { Logger.error(this, "Mailsite is missing public key modulus"); schedule(1, TimeUnit.HOURS); return; } String keyExponent = mailsiteProps.get("asymkey.pubexponent"); if (keyExponent == null) { Logger.error(this, "Mailsite is missing public key exponent"); schedule(1, TimeUnit.HOURS); return; } byte[] rtsMessage = encryptMessage(signedMessage, keyModulus, keyExponent); //Clean up the fetched mailsite mailsiteProps = null; if (!mailsite.delete()) { Logger.error(this, "Couldn't delete " + mailsite); } //Insert int slot; try { String key = "KSK@" + rtsKey + "-" + DateStringFactory.getKeyString(); Logger.debug(this, "Inserting RTS to " + key); slot = fcpClient.slotInsert(rtsMessage, key, 1, ""); } catch (ConnectionTerminatedException e) { return; } if (slot < 0) { Logger.debug(this, "Slot insert failed, trying again in 5 minutes"); schedule(TASK_RETRY_DELAY, TimeUnit.MILLISECONDS); return; } //Update channel props file synchronized (channelProps) { //Check if we've gotten the CTS while inserting the RTS if (!"cts-received".equals(channelProps.get(PropsKeys.SENDER_STATE))) { channelProps.put(PropsKeys.SENDER_STATE, "rts-sent"); } channelProps.put(PropsKeys.RTS_SENT_AT, Long.toString(System.currentTimeMillis())); } long delay = sendRTSIn(); if (delay < 0) { return; } Logger.debug(this, "Rescheduling RTSSender to run in " + delay + " ms when the reinsert is due"); try { schedule(delay, TimeUnit.MILLISECONDS); } catch (RejectedExecutionException e) { Logger.debug(this, "Caugth RejectedExecutionException while scheduling RTSSender"); } //Start the fetcher now that we have keys, slots etc. fetcher.execute(); } public void execute() { Logger.debug(this, "Scheduling RTSSender for execution"); try { executor.execute(this); } catch (RejectedExecutionException e) { Logger.debug(this, "Caugth RejectedExecutionException while scheduling RTSSender"); } } public void schedule(long delay, TimeUnit unit) { Logger.debug(this, "Scheduling RTSSender for execution in " + delay + " " + unit.toString().toLowerCase(Locale.ROOT)); try { executor.schedule(this, delay, unit); } catch (RejectedExecutionException e) { Logger.debug(this, "Caugth RejectedExecutionException while scheduling RTSSender"); } } /** * Returns the number of milliseconds left to when the RTS should be sent, or -1 if it * should not be sent. * @return the number of milliseconds left to when the RTS should be sent */ private long sendRTSIn() { //Check if the CTS has been received String senderState; String recipientState; synchronized (channelProps) { senderState = channelProps.get(PropsKeys.SENDER_STATE); recipientState = channelProps.get(PropsKeys.RECIPIENT_STATE); } if ((senderState != null) && senderState.equals("cts-received")) { Logger.debug(this, "Won't send RTS, CTS has been received"); return -1; } if ((recipientState != null) && (recipientState.equals("rts-received") || recipientState.equals("cts-sent"))) { //We've received an RTS from the other side Logger.debug(this, "Won't send RTS, RTS has been received from other side"); return -1; } //Check when the RTS should be (re)sent String rtsSentAt; synchronized (channelProps) { rtsSentAt = channelProps.get(PropsKeys.RTS_SENT_AT); } if (rtsSentAt != null) { long sendTime; try { sendTime = Long.parseLong(rtsSentAt); } catch (NumberFormatException e) { Logger.error(this, "Illegal value in " + PropsKeys.RTS_SENT_AT + " field (" + rtsSentAt + "), assuming 0"); sendTime = 0; } long timeToResend = (24 * 60 * 60 * 1000) - (System.currentTimeMillis() - sendTime); if (timeToResend > 0) { return timeToResend; } } //Send the RTS immediately return 0; } private byte[] buildRTSMessage(String senderMailsiteKey, String recipientIdentityID, String channelPrivateKey, String initiatorSlot, String responderSlot, long timeout) { assert (FreenetURI.checkUSK(senderMailsiteKey)) : "Malformed sender mailsite: " + senderMailsiteKey; assert (recipientIdentityID != null); assert (channelPrivateKey.matches("^SSK@\\S{42,44},\\S{42,44},\\S{7}/$")) : "Malformed channel key: " + channelPrivateKey; assert (initiatorSlot != null); assert (responderSlot != null); //Allow sending the RTS 1 day into the read-only period. This shouldn't happen, but //shouldn't cause any problems either so allow plenty of time to avoid false positives if (System.currentTimeMillis() > (timeout + (24 * 60 * 60 * 1000))) { Logger.warning(this, "Building RTS when channel is past final timeout (" + "read only=" + timeout + ", final timeout=" + (timeout + CHANNEL_TIMEOUT) + ", current time=" + System.currentTimeMillis() + ")"); } StringBuffer rtsMessage = new StringBuffer(); rtsMessage.append(RTSKeys.MAILSITE + "=" + senderMailsiteKey + "\r\n"); rtsMessage.append(RTSKeys.TO + "=" + recipientIdentityID + "\r\n"); rtsMessage.append(RTSKeys.CHANNEL + "=" + channelPrivateKey + "\r\n"); rtsMessage.append(RTSKeys.INITIATOR_SLOT + "=" + initiatorSlot + "\r\n"); rtsMessage.append(RTSKeys.RESPONDER_SLOT + "=" + responderSlot + "\r\n"); rtsMessage.append(RTSKeys.TIMEOUT + "=" + timeout + "\r\n"); rtsMessage.append("\r\n"); byte[] rtsMessageBytes; try { rtsMessageBytes = rtsMessage.toString().getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { Logger.error(this, "JVM doesn't support UTF-8 charset", e); return null; } return rtsMessageBytes; } private byte[] signRtsMessage(byte[] rtsMessageBytes) { SHA256Digest sha256 = new SHA256Digest(); sha256.update(rtsMessageBytes, 0, rtsMessageBytes.length); byte[] hash = new byte[sha256.getDigestSize()]; sha256.doFinal(hash, 0); RSAKeyParameters ourPrivateKey = AccountManager.getPrivateKey(account.getProps()); AsymmetricBlockCipher signatureCipher = new RSAEngine(); signatureCipher.init(true, ourPrivateKey); byte[] signature = null; try { signature = signatureCipher.processBlock(hash, 0, hash.length); } catch (InvalidCipherTextException e) { Logger.error(this, "Failed to RSA encrypt hash: " + e.getMessage(), e); return null; } byte[] signedMessage = new byte[rtsMessageBytes.length + signature.length]; System.arraycopy(rtsMessageBytes, 0, signedMessage, 0, rtsMessageBytes.length); System.arraycopy(signature, 0, signedMessage, rtsMessageBytes.length, signature.length); return signedMessage; } private byte[] encryptMessage(byte[] signedMessage, String keyModulus, String keyExponent) { //Make a new symmetric key for the message byte[] aesKeyAndIV = new byte[32 + 16]; Freemail.getRNG().nextBytes(aesKeyAndIV); //Encrypt the message with the new symmetric key PaddedBufferedBlockCipher aesCipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()), new PKCS7Padding()); KeyParameter aesKeyParameters = new KeyParameter(aesKeyAndIV, 16, 32); ParametersWithIV aesParameters = new ParametersWithIV(aesKeyParameters, aesKeyAndIV, 0, 16); aesCipher.init(true, aesParameters); byte[] encryptedMessage = new byte[aesCipher.getOutputSize(signedMessage.length)]; int offset = aesCipher.processBytes(signedMessage, 0, signedMessage.length, encryptedMessage, 0); try { aesCipher.doFinal(encryptedMessage, offset); } catch (InvalidCipherTextException e) { Logger.error(this, "Failed to perform symmetric encryption on RTS data: " + e.getMessage(), e); return null; } RSAKeyParameters recipientPublicKey = new RSAKeyParameters(false, new BigInteger(keyModulus, 32), new BigInteger(keyExponent, 32)); AsymmetricBlockCipher keyCipher = new RSAEngine(); keyCipher.init(true, recipientPublicKey); byte[] encryptedAesParameters = null; try { encryptedAesParameters = keyCipher.processBlock(aesKeyAndIV, 0, aesKeyAndIV.length); } catch (InvalidCipherTextException e) { Logger.error(this, "Failed to perform asymmetric encryption on RTS symmetric key: " + e.getMessage(), e); return null; } //Assemble the final message byte[] rtsMessage = new byte[encryptedAesParameters.length + encryptedMessage.length]; System.arraycopy(encryptedAesParameters, 0, rtsMessage, 0, encryptedAesParameters.length); System.arraycopy(encryptedMessage, 0, rtsMessage, encryptedAesParameters.length, encryptedMessage.length); return rtsMessage; } @Override public String toString() { return "RTSSender [" + channelDir + "]"; } } private boolean handleMessage(File msg) { // parse the Freemail header(s) out. PropsFile msgprops = PropsFile.createPropsFile(msg, true); String s_id = msgprops.get("id"); if (s_id == null) { Logger.error(this, "Message is missing id. Discarding."); msgprops.closeReader(); return true; } long id; try { id = Long.parseLong(s_id); } catch (NumberFormatException nfe) { /* FIXME: Id doesn't have to be an integer */ Logger.error(this, "Got a message with an invalid (non-integer) id. Discarding."); msgprops.closeReader(); return true; } long ackDelay = (long) (System.currentTimeMillis() + (Math.random() * MAX_ACK_DELAY)); synchronized (ackLog) { try { ackLog.add(id, Long.toString(ackDelay)); } catch (IOException e) { Logger.error(this, "Caugth IOException while writing to ack log: " + e.getMessage(), e); return false; } } BufferedReader br = msgprops.getReader(); if (br == null) { Logger.error(this, "Got an invalid message. Discarding."); msgprops.closeReader(); return true; } if (!channelEventCallback.get().handleMessage(this, br, id)) { return false; } try { ScheduledExecutorService senderExecutor = freemail.getExecutor(TaskType.SENDER); senderExecutor.execute(new AckInserter(id, ackDelay)); } catch (RejectedExecutionException e) { Logger.debug(this, "Caugth RejectedExecutionException while scheduling AckInserter"); } return true; } private class AckInserter implements Runnable { private final long ackId; private final long insertAfter; private AckInserter(long ackId, long insertAfter) { this.ackId = ackId; this.insertAfter = insertAfter; } @Override public void run() { Logger.debug(this, "AckInserter(" + ackId + ") for " + Channel.this.toString() + " running"); if (System.currentTimeMillis() < insertAfter) { long remaining = insertAfter - System.currentTimeMillis(); Logger.debug(this, "Rescheduling in " + remaining + "ms when inserting is allowed"); ScheduledExecutorService senderExecutor = freemail.getExecutor(TaskType.SENDER); senderExecutor.schedule(this, remaining, TimeUnit.MILLISECONDS); return; } //Build the header of the inserted message String header = "messagetype=ack\r\n" + "id=" + ackId + "\r\n" + "\r\n"; Bucket bucket; try { bucket = new ArrayBucket(header.getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { //JVMs are required to support UTF-8, so we can assume it is always available throw new AssertionError("JVM doesn't support UTF-8 charset"); } boolean inserted; try { inserted = insertMessage(bucket, "ack" + ackId); } catch (IOException e) { //The getInputStream() method of ArrayBucket doesn't throw throw new AssertionError("getInputStream() method of ArrayBucket threw IOException"); } catch (InterruptedException e) { Logger.debug(this, "AckInserter interrupted, quitting"); return; } if (inserted) { synchronized (ackLog) { try { ackLog.remove(ackId); } catch (IOException e) { Logger.error(this, "Caugth IOException while writing to ack log: " + e.getMessage(), e); } } } else { try { ScheduledExecutorService senderExecutor = freemail.getExecutor(TaskType.SENDER); senderExecutor.schedule(this, TASK_RETRY_DELAY, TimeUnit.MILLISECONDS); } catch (RejectedExecutionException e) { Logger.debug(this, "Caugth RejectedExecutionException while scheduling AckInserter"); } } } } private boolean handleAck(File result) { PropsFile ackProps = PropsFile.createPropsFile(result); String ackString = ackProps.get("id"); if (ackString == null) { Logger.error(this, "Received ack without id, discarding"); return true; } Logger.debug(this, "Got ack with id " + ackString); String[] acks = ackString.split(","); for (String ack : acks) { long messageId = Long.parseLong(ack); channelEventCallback.get().onAckReceived(messageId); } return true; } private class HashSlotManager extends SlotManager { HashSlotManager(SlotSaveCallback cb, Object userdata, String slotlist) { super(cb, userdata, slotlist); } @Override protected String incSlot(String slot) { return calculateNextSlot(slot); } } private static class ChannelSlotSaveImpl implements SlotSaveCallback { private final PropsFile propsFile; private final String keyName; private ChannelSlotSaveImpl(PropsFile propsFile, String keyName) { this.propsFile = propsFile; this.keyName = keyName; } @Override public void saveSlots(String slots, Object userdata) { synchronized (propsFile) { propsFile.put(keyName, slots); } } } public interface ChannelEventCallback { public void onAckReceived(long id); public boolean handleMessage(Channel channel, BufferedReader message, long id); } }