/* Class to manage reading from and writing Freenet WORM Archives.
*
* 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 fniki.wiki;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import fmsutil.FMSUtil;
import wormarc.Archive;
import wormarc.AuditArchive;
import wormarc.ExternalRefs;
import wormarc.FileManifest;
import wormarc.IOUtil;
import wormarc.LinkDigest;
import wormarc.RootObjectKind;
import wormarc.io.FreenetIO;
public class ArchiveManager {
public final static String FCP_HOST = "127.0.0.1";
public final static int FCP_PORT = 9481;
public final static String FMS_HOST = "127.0.0.1";
public final static int FMS_PORT = 1119;
public final static String FMS_GROUP = "biss.test000";
public final static String BISS_NAME = "testwiki";
// Maximum number of versions to read from FMS.
private final static int MAX_VERSIONS = 50;
String mFcpHost = FCP_HOST;
int mFcpPort = FCP_PORT;
String mFmsHost = FMS_HOST;
int mFmsPort = FMS_PORT;
String mFmsGroup = FMS_GROUP;
String mBissName= BISS_NAME;
String mPrivateSSK;
String mFmsId;
// Base64 SSK public key hash to FMS name. i.e. the part before '@'.
Map<String, String> mNymLut = new HashMap<String, String>();
// LATER: Revisit. This was HACK to work around the fact that GetCHKOnly=true
// is broken for splitfiles.
//
// Block hex digest to CHK key map.
Map<String, String> mSha1ToChk = new HashMap<String, String>();
String mParentUri;
Archive mArchive;
FileManifest mFileManifest;
LocalWikiChanges mOverlay;
public void setDebugOutput(PrintStream out) {
FreenetIO.setDebugOutput(out);
}
private static void validatePrivateSSK(String value) {
if (!value.startsWith("SSK@") || !value.endsWith(",AQECAAE/")) {
throw new IllegalArgumentException("That doesn't look like a private SSK. " +
"Did you forget the trailing '/'?");
}
}
public void setPrivateSSK(String value) {
validatePrivateSSK(value);
mPrivateSSK = value;
}
public String invertPrivateSSK(String value, int timeoutMs) throws IOException {
validatePrivateSSK(value);
return makeIO().invertPrivateSSK(value, timeoutMs);
}
public String getPrivateSSK() { return mPrivateSSK; }
public String getParentUri() { return mParentUri; }
public void setFmsId(String value) {
if (value.indexOf("@") != -1) {
throw new IllegalArgumentException("FMS Id Should only include the part before the '@'!");
}
mFmsId = value;
}
public String getFmsId() { return mFmsId; }
public void setFcpHost(String value) {mFcpHost = value; }
public String getFcpHost() { return mFcpHost; }
public void setFcpPort(int value) {mFcpPort = value; }
public int getFcpPort() { return mFcpPort; }
public void setFmsHost(String value) { mFmsHost = value; }
public String getFmsHost() { return mFmsHost; }
public void setFmsPort(int value) { mFmsPort = value; }
public int getFmsPort() { return mFmsPort; }
public void setFmsGroup(String value) { mFmsGroup = value; }
public String getFmsGroup() { return mFmsGroup; }
public void setBissName(String value) { mBissName= value; }
public String getBissName() { return mBissName; }
// DCI: Fix this to roll back state on exceptions.
public void load(String uri) throws IOException {
FreenetIO io = makeIO();
io.setRequestUri(uri);
Archive archive = Archive.load(io);
validateUriHashes(archive, uri, true);
mArchive = archive;
mFileManifest = FileManifest.fromArchiveRootObject(mArchive);
mOverlay = new LocalWikiChanges(mArchive, mFileManifest); // DCI: why copy ?
mParentUri = uri;
}
public void createEmptyArchive() throws IOException {
mArchive = new Archive();
mFileManifest = FileManifest.fromArchiveRootObject(mArchive);
mOverlay = new LocalWikiChanges(mArchive, mFileManifest); // DCI: why copy ?
mParentUri = null;
}
public static class UpToDateException extends IOException {
public UpToDateException() {
super("There are no local changes to submit.");
}
}
private FreenetIO makeIO() {
return new FreenetIO(mFcpHost, mFcpPort, null, mSha1ToChk);
}
// The name of a jfniki archive includes the hash of the
// full archive manifest file, and hashes of the SSK of
// it's parent(s).
//
// use '_' instead of '|' because '|' gets percent escaped.
// <sha1_of_am_file>[<underbar>sha1_of_parent_ssk]
private static String makeUriNamePart(Archive archive) throws IOException {
// Generate a unique SSK.
LinkDigest digest = archive.getRootObject(RootObjectKind.ARCHIVE_MANIFEST);
// The hash of the actual file, not just the chain head SHA.
LinkDigest fileHash = IOUtil.getFileDigest(archive.getFile(digest));
String parentKeyHashes = "";
LinkDigest refsDigest = archive.getRootObject(RootObjectKind.PARENT_REFERENCES);
if (!refsDigest.isNullDigest()) {
ExternalRefs refs = ExternalRefs.fromBytes(archive.getFile(refsDigest));
for (ExternalRefs.Reference ref : refs.mRefs) {
if (ref.mKind != ExternalRefs.KIND_FREENET) {
continue;
}
parentKeyHashes += "_";
parentKeyHashes += IOUtil.getFileDigest(IOUtil.toStreamAsUtf8(ref.mExternalKey))
.hexDigest(8);
}
}
return fileHash.hexDigest(8) + parentKeyHashes;
}
private static void validateUriHashes(Archive archive,
String uri,
boolean allowNoParents) throws IOException {
String[] fields = uri.split("/");
if (fields.length != 2) {
throw new IOException("Couldn't parse uri: " + uri);
}
fields = fields[1].split("_");
if (fields.length < 1) {
throw new IOException("Couldn't parse uri: " + uri);
}
String[] expected = makeUriNamePart(archive).split("_");
for (int index = 0; index < expected.length; index++) {
if (index >= fields.length) {
continue;
}
}
if (fields.length == 1 && fields[0].equals(expected[0])) {
// LATER: tighten up.
// For now, allow old URIs that don't have parent info hashes.
return;
}
if (expected.length != fields.length) {
throw new IOException("Hash validation failed(0)! Inserter is lying about contents.");
}
for (int index = 0; index < expected.length; index++) {
if (!expected[index].equals(fields[index])) {
throw new IOException("Hash validation failed(1)! Inserter is lying about contents.");
}
}
}
private String getInsertUri(Archive archive) throws IOException {
String uri = mPrivateSSK + makeUriNamePart(archive);
validateUriHashes(archive, uri, false);
return uri;
}
// DCI: commitAndPushToFreenet() ?
public String pushToFreenet(PrintStream out) throws IOException {
FileManifest.Changes changes = mFileManifest.diffTo(mArchive, mOverlay);
if (changes.isUnmodified()) {
throw new IOException("Didn't find any local changes to submit.");
}
// Copy so that we can cleanly role back if an exception is raised.
Archive copy = mArchive.deepCopy();
FileManifest files = FileManifest.fromArchiveRootObject(copy);
// Commit local changes to the Archive.
copy.unsetRootObject(RootObjectKind.PARENT_REFERENCES);
// Update the archive
copy.startUpdate();
files.updateFrom(copy, mOverlay);
LinkDigest digest = copy.updateRootObject(files.toBytes(), RootObjectKind.FILE_MANIFEST);
if (mParentUri != null) {
out.println("Set PARENT_REFERENCES: " + mParentUri);
List<String> keys = Arrays.asList(mParentUri);
LinkDigest refs =
copy.updateRootObject(ExternalRefs.create(keys, ExternalRefs.KIND_FREENET)
.toBytes(),
RootObjectKind.PARENT_REFERENCES);
}
copy.commitUpdate();
copy.compressAndUpdateArchiveManifest();
// Generate a unique SSK based on the SHA hash of the archive manifest.
String insertUri = getInsertUri(copy);
out.println("Insert URI: " + insertUri);
// Push the updated version into Freenet.
FreenetIO io = makeIO();
io.setInsertUri(insertUri);
out.println("Writing to Freenet...");
copy.write(io);
if (mFmsId != null) {
try {
out.println("Sending FMS update notification to: " + mFmsGroup);
FMSUtil.sendBISSMsg(mFmsHost, mFmsPort, mFmsId, mFmsGroup,
mBissName, io.getRequestUri());
} catch (IOException ioe) {
out.println("FMS send failed: " + ioe.getMessage());
}
}
// Don't update any state until all calls which could raise have finished.
mArchive = copy;
mFileManifest = FileManifest.fromArchiveRootObject(mArchive);
mOverlay = new LocalWikiChanges(mArchive, mFileManifest);
mParentUri = io.getRequestUri();
out.println("Request URI: " + mParentUri);
return mParentUri;
}
public FileManifest.Changes getLocalChanges() throws IOException {
return mFileManifest.diffTo(mArchive, mOverlay);
}
public void readChangeLog(PrintStream out,
AuditArchive.ChangeLogCallback callback) throws IOException {
if (mParentUri == null) {
throw new IOException("URI not set!");
}
ExternalRefs.Reference head =
new ExternalRefs.Reference(ExternalRefs.KIND_FREENET, mParentUri);
FreenetIO freenetResolver = makeIO();
Archive archive = freenetResolver.resolve(head);
AuditArchive.getManifestChangeLog(head, archive, freenetResolver, callback);
}
public List<FMSUtil.BISSRecord> getRecentWikiVersions(PrintStream out) throws IOException {
List<FMSUtil.BISSRecord> records =
FMSUtil.getBISSRecords(mFmsHost, mFmsPort, mFmsId, mFmsGroup, mBissName, MAX_VERSIONS);
// LATER: do better.
for (FMSUtil.BISSRecord record : records) {
String fields[] = record.mFmsId.split("@");
if (fields.length != 2) {
continue;
}
mNymLut.put(fields[1].trim(), fields[0].trim());
}
return records;
}
public WikiTextStorage getStorage() throws IOException {
if (mOverlay == null) {
throw new IllegalStateException("No archive loaded!");
}
return mOverlay;
}
public String getNym(String sskRequestUri, boolean showPublicKey) {
int start = sskRequestUri.indexOf("@");
int end = sskRequestUri.indexOf(",");
if (start == -1 || end == -1 || start >= end) {
return "???";
}
String publicKeyHash = sskRequestUri.substring(start + 1, end - start + 3);
// SSK@THIS_PART,
String nym = mNymLut.get(publicKeyHash);
if (nym == null) {
nym = "???";
}
if (!showPublicKey) {
return nym;
}
return nym + "@" + publicKeyHash;
}
}