/* An Archive.IO and ArchiveResolver implementation which read and writes to Freenet. * * Copyright (C) 2010, 2011 Darrell Karbott * * This library 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.0 of the License, or (at your option) any later version. * * This library 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 library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks * * This file was developed as component of * "fniki" (a wiki implementation running over Freenet). */ package wormarc.io; import java.io.InputStream; import java.io.IOException; import java.io.PrintStream; import java.util.Arrays; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import wormarc.Archive; import wormarc.ArchiveResolver; import wormarc.BinaryLinkRep; import wormarc.Block; import wormarc.ExternalRefs; import wormarc.HistoryLink; import wormarc.HistoryLinkMap; import wormarc.IOUtil; import wormarc.LinkDataFactory; import wormarc.LinkDigest; import wormarc.RootObjectKind; public class FreenetIO implements Archive.IO, ArchiveResolver { private LinkCache mCache; // Transient private HistoryLinkMap mLinkMap; private LinkDataFactory mLinkDataFactory; private String mHost; private int mPort; private String mClientName = "FreenetIO_"; private int mMaxBlockLength = 8 * 1024 * 1024; private int mMaxBlockCount = 4; private String mInsertUri; private String mRequestUri; private FreenetTopKey mPreviousTopKey; private static PrintStream sDebugOut = System.err; // Cache can be null. // When it is non-null all links read from Freenet are dumped to the cache. public FreenetIO(String host, int port, LinkCache cache) { mHost = host; mPort = port; mCache = cache; } public FreenetIO(String host, int port) { this(host, port, null); } public static void setDebugOutput(PrintStream out) { synchronized(FreenetIO.class) { sDebugOut = out; FCPCommandRunner.setDebugOutput(sDebugOut); } } public String getInsertUri() { return mInsertUri; } public void setInsertUri(String uri) { mInsertUri = uri; } public String getRequestUri() { return mRequestUri; } public void setRequestUri(String uri) { mRequestUri = uri; } public FreenetTopKey readTopKey(String uri) throws IOException { FCPCommandRunner runner = null; try { runner = new FCPCommandRunner(mHost, mPort, mClientName + IOUtil.randomHexString(12)); FCPCommandRunner.GetTopKey requestTopKey = runner.sendGetTopKey(uri); runner.waitUntilAllFinished(); requestTopKey.raiseOnFailure(); return requestTopKey.getTopKey(); } catch (InterruptedException ie) { throw new IOException("FreenetTopKey read timed out.", ie); } finally { if (runner != null) { runner.disconnect(); } } } // Speeds up inserting by allowing write() to skip blocks that were // already inserted. // For this to work, the PARENT_REFERENCES root object must be up to date. public void maybeLoadPreviousTopKey(Archive archive) throws IOException { mPreviousTopKey = null; if (archive.getRootObject(RootObjectKind.PARENT_REFERENCES).isNullDigest()) { return; } ExternalRefs refs = ExternalRefs.fromBytes(archive. getFile(archive. getRootObject(RootObjectKind.PARENT_REFERENCES))); if (refs.mRefs.size() != 1 || refs.mRefs.get(0).mKind != ExternalRefs.KIND_FREENET) { throw new IOException("Expected a single Freenet URI!"); // LATER: Must remove this constraint to allow merging. } String topKeyUri = refs.mRefs.get(0).mExternalKey; mPreviousTopKey = readTopKey(topKeyUri); } // DCI: BUG: redundant inserts are not supported yet. False assumption Block <-> CHK // Updates the request URI on success. public void write(HistoryLinkMap linkMap, List<Block> blocks, List<Archive.RootObject> rootObjects) throws IOException { if (mInsertUri == null) { throw new IllegalStateException("Set the uri!"); } // DCI: fail early for inserts that are too big. FCPCommandRunner runner = null; try { runner = new FCPCommandRunner(mHost, mPort, mClientName + IOUtil.randomHexString(12)); // Precompute the block CHKs so we can skip blocks that // are already in Freenet. List<FreenetTopKey.BlockDescription> descriptions = precomputeDescriptions(runner, linkMap, blocks); if (blocks.size() != descriptions.size()) { throw new RuntimeException("Assertion Failure: blocks.size() != descriptions.size()"); } Set<String> previousChks = new HashSet<String>(); if (mPreviousTopKey != null) { for (FreenetTopKey.BlockDescription desc : mPreviousTopKey.mBlockDescriptions) { for (int subIndex = 0; subIndex < desc.mCHKs.size(); subIndex++) { previousChks.add(desc.getCHK(subIndex)); } } } List<FCPCommandRunner.PutBlock> puts = new ArrayList<FCPCommandRunner.PutBlock>(); for (int index = 0; index < descriptions.size(); index++) { FreenetTopKey.BlockDescription desc = descriptions.get(index); if (previousChks.contains(desc.getCHK(0))) { // i.e. the block was already inserted, so skip it. continue; } puts.add(runner.sendPutBlock(index, linkMap, blocks.get(index))); } runner.waitUntilAllFinished(); for (FCPCommandRunner.PutBlock put : puts) { put.raiseOnFailure(); } FreenetTopKey topKey = new FreenetTopKey(rootObjects, descriptions); raiseOnSuspectTopKey(topKey); // Fails, but too late! FCPCommandRunner.PutTopKey putTopKey = runner.sendPutTopKey(mInsertUri, topKey); runner.waitUntilAllFinished(); putTopKey.raiseOnFailure(); mRequestUri = putTopKey.getUri(); } catch (InterruptedException ie) { throw new IOException("Write timed out.", ie); } catch (IllegalBase64Exception ibe) { throw new IOException("Binary URI decode failed", ibe); } finally { if (runner != null) { runner.disconnect(); } } } private void raiseOnSuspectTopKey(FreenetTopKey topKey) throws IOException { if (topKey.mBlockDescriptions.size() > mMaxBlockCount) { throw new IOException(String.format("To many blocks in FreenetTopKey: %d", topKey.mBlockDescriptions.size())); } for (FreenetTopKey.BlockDescription desc : topKey.mBlockDescriptions ) { if (desc.mLength > mMaxBlockLength) { throw new IOException(String.format("Block too big: %d", desc.mLength)); } } int length = IOUtil.readAndClose(topKey.toBytes()).length; if (length > FreenetTopKey.MAX_LENGTH) { throw new IOException("FreenetTopKey is too big!"); } } public Archive.ArchiveData read(HistoryLinkMap linkMap, LinkDataFactory linkFactory) throws IOException { // For now, we request everything. // LATER: Think through incremental requesting. We can do much better. if (linkMap == null) { throw new IllegalArgumentException("linkMap == null"); } if (linkFactory == null) { throw new IllegalArgumentException("linkFactory == null"); } FCPCommandRunner runner = null; try { // Read topkey mLinkMap = linkMap; mLinkDataFactory = linkFactory; runner = new FCPCommandRunner(mHost, mPort, mClientName + IOUtil.randomHexString(12)); // Read the topkey from Freenet. FCPCommandRunner.GetTopKey requestTopKey = runner.sendGetTopKey(mRequestUri); runner.waitUntilAllFinished(); requestTopKey.raiseOnFailure(); FreenetTopKey topKey = requestTopKey.getTopKey(); raiseOnSuspectTopKey(topKey); // Read all the blocks listed in the top key. // Note: The GetBlock requests read and cache the links // by calling back into readLinks(). See below. int count = 0; List<FCPCommandRunner.GetBlock> gets = new ArrayList<FCPCommandRunner.GetBlock>(); for (FreenetTopKey.BlockDescription desc : topKey.mBlockDescriptions ) { // LATER: Handle redundant block fetches. sDebugOut.println(String.format("Requesting[%d]: %s", desc.mLength, desc.getCHK(0))); gets.add(runner.sendGetBlock(desc.getCHK(0), desc.mLength, count++, this)); } runner.waitUntilAllFinished(); // Collect the Blocks. List<Block> blocks = new ArrayList<Block>(); for (FCPCommandRunner.GetBlock get : gets) { get.raiseOnFailure(); blocks.add(get.getBlock()); } return new Archive.ArchiveData(blocks, topKey.mRootObjects); } catch (InterruptedException ie) { throw new IOException("Read timed out.", ie); } catch (IllegalBase64Exception ibe) { throw new IOException("Binary URI decode failed", ibe); } finally { mLinkMap = null; mLinkDataFactory = null; if (runner != null) { sDebugOut.println("FCP Connection -- DISCONNECTING!"); runner.disconnect(); } } } // Used by FCPCommandRunner. protected Block readLinks(InputStream data) throws IOException { if (mLinkDataFactory == null || mLinkMap == null) { throw new IllegalStateException("Not expecting call."); } List<LinkDigest> digests = new ArrayList<LinkDigest>(); while (true) { HistoryLink link = BinaryLinkRep.fromBytes(data, mLinkDataFactory); if (link == null) { break; } digests.add(link.mHash); mLinkMap.addLink(link); if (mCache != null) { mCache.writeLink(link); } } return new Block(digests); } //////////////////////////////////////////////////////////// private List<FreenetTopKey.BlockDescription> precomputeDescriptions(FCPCommandRunner runner, HistoryLinkMap linkMap, List<Block> blocks) throws IllegalBase64Exception, InterruptedException, IOException { // Use the Freenet node to tell us the CHKs for the new blocks without // inserting them. int count = 0; List<FCPCommandRunner.GetBlockChk> getChks = new ArrayList<FCPCommandRunner.GetBlockChk>(); for (Block block : blocks) { getChks.add(runner.sendGetBlockChk(count++, linkMap, block)); } runner.waitUntilAllFinished(); int index = 0; List<FreenetTopKey.BlockDescription> descriptions = new ArrayList<FreenetTopKey.BlockDescription>(); for (FCPCommandRunner.GetBlockChk get : getChks) { get.raiseOnFailure(); descriptions.add(FreenetTopKey.makeDescription(get.getLength(), Arrays.asList(get.getUri()))); index++; } return descriptions; } //////////////////////////////////////////////////////////// public Archive resolve(ExternalRefs.Reference fromReference) throws IOException { String previousRequestUri = mRequestUri; try { if (fromReference.mKind != ExternalRefs.KIND_FREENET) { throw new IOException("Reference is not a Freenet URI"); } sDebugOut.println("resolving Archive from: " + fromReference.mExternalKey); mRequestUri = fromReference.mExternalKey; Archive loaded = Archive.load(this); // Hmmmm... slurps stuff into the cache. ??? if (!loaded.getRootObject(RootObjectKind.ARCHIVE_MANIFEST).isNullDigest()) { if (!loaded.hasValidArchiveManifest()) { throw new IOException("Invalid ARCHIVE_MANIFEST: " + fromReference.mExternalKey); } } return loaded; } finally { mRequestUri = previousRequestUri; } } public String getNym(ExternalRefs.Reference fromReference) throws IOException { if (fromReference.mKind != ExternalRefs.KIND_FREENET || (!fromReference.mExternalKey.startsWith("SSK@") || fromReference.mExternalKey.indexOf("/") == -1)) { return "notfreenetssk"; } // Public key part of the SSK. return fromReference.mExternalKey. substring(4, fromReference.mExternalKey.indexOf(",")); } }