diff --git a/java/org/servalproject/servaldna/ServalDClient.java b/java/org/servalproject/servaldna/ServalDClient.java index afec9e39..a5a89cb1 100644 --- a/java/org/servalproject/servaldna/ServalDClient.java +++ b/java/org/servalproject/servaldna/ServalDClient.java @@ -25,6 +25,7 @@ import org.servalproject.servaldna.meshms.MeshMSConversationList; import org.servalproject.servaldna.meshms.MeshMSException; import org.servalproject.servaldna.meshms.MeshMSMessageList; +import java.io.InputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URL; @@ -36,11 +37,19 @@ import org.servalproject.servaldna.BundleId; import org.servalproject.servaldna.ServalDCommand; import org.servalproject.servaldna.ServalDInterfaceException; import org.servalproject.servaldna.rhizome.RhizomeCommon; +import org.servalproject.servaldna.rhizome.RhizomeIncompleteManifest; import org.servalproject.servaldna.rhizome.RhizomeBundleList; import org.servalproject.servaldna.rhizome.RhizomeManifestBundle; import org.servalproject.servaldna.rhizome.RhizomePayloadRawBundle; import org.servalproject.servaldna.rhizome.RhizomePayloadBundle; +import org.servalproject.servaldna.rhizome.RhizomeInsertBundle; import org.servalproject.servaldna.rhizome.RhizomeException; +import org.servalproject.servaldna.rhizome.RhizomeInvalidManifestException; +import org.servalproject.servaldna.rhizome.RhizomeFakeManifestException; +import org.servalproject.servaldna.rhizome.RhizomeInconsistencyException; +import org.servalproject.servaldna.rhizome.RhizomeEncryptionException; +import org.servalproject.servaldna.rhizome.RhizomeReadOnlyException; +import org.servalproject.servaldna.rhizome.RhizomeDecryptionException; import org.servalproject.servaldna.meshms.MeshMSCommon; import org.servalproject.servaldna.meshms.MeshMSConversationList; import org.servalproject.servaldna.meshms.MeshMSMessageList; @@ -87,6 +96,30 @@ public class ServalDClient implements ServalDHttpConnectionFactory return RhizomeCommon.rhizomePayload(this, bid); } + public RhizomeInsertBundle rhizomeInsert(SubscriberId author, RhizomeIncompleteManifest manifest) + throws ServalDInterfaceException, + IOException, + RhizomeInvalidManifestException, + RhizomeFakeManifestException, + RhizomeInconsistencyException, + RhizomeReadOnlyException, + RhizomeEncryptionException + { + return RhizomeCommon.rhizomeInsert(this, author, manifest); + } + + public RhizomeInsertBundle rhizomeInsert(SubscriberId author, RhizomeIncompleteManifest manifest, InputStream payloadStream, String fileName) + throws ServalDInterfaceException, + IOException, + RhizomeInvalidManifestException, + RhizomeFakeManifestException, + RhizomeInconsistencyException, + RhizomeReadOnlyException, + RhizomeEncryptionException + { + return RhizomeCommon.rhizomeInsert(this, author, manifest, payloadStream, fileName); + } + public MeshMSConversationList meshmsListConversations(SubscriberId sid) throws ServalDInterfaceException, IOException, MeshMSException { MeshMSConversationList list = new MeshMSConversationList(this, sid); diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeBundleStatus.java b/java/org/servalproject/servaldna/rhizome/RhizomeBundleStatus.java index 92d817a4..16e5f60f 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeBundleStatus.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeBundleStatus.java @@ -33,7 +33,8 @@ public enum RhizomeBundleStatus { INVALID(4), // manifest is invalid FAKE(5), // manifest signature not valid INCONSISTENT(6), // manifest filesize/filehash does not match supplied payload - NO_ROOM(7) // doesn't fit; store may contain more important bundles + NO_ROOM(7), // doesn't fit; store may contain more important bundles + READONLY(8) // cannot modify manifest; secret unknown ; final public int code; @@ -55,6 +56,7 @@ public enum RhizomeBundleStatus { case 5: status = FAKE; break; case 6: status = INCONSISTENT; break; case 7: status = NO_ROOM; break; + case 8: status = READONLY; break; default: throw new InvalidException(code); } assert status.code == code; diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java index 30c81dc8..95f2ba5d 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java @@ -31,6 +31,7 @@ import java.io.IOException; import java.io.PrintStream; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; import java.net.URL; import java.net.HttpURLConnection; import org.servalproject.json.JSONTokeniser; @@ -57,6 +58,17 @@ public class RhizomeCommon String payload_status_message; } + private static void dumpStatus(Status status, PrintStream out) + { + out.println("input_stream=" + status.input_stream); + out.println("http_status_code=" + status.http_status_code); + out.println("http_status_message=" + status.http_status_message); + out.println("bundle_status_code=" + status.bundle_status_code); + out.println("bundle_status_message=" + status.bundle_status_message); + out.println("payload_status_code=" + status.payload_status_code); + out.println("payload_status_message=" + status.payload_status_message); + } + protected static Status receiveResponse(HttpURLConnection conn, int expected_response_code) throws IOException, ServalDInterfaceException { int[] expected_response_codes = { expected_response_code }; @@ -84,9 +96,9 @@ public class RhizomeCommon throw new ServalDInterfaceException("unexpected HTTP response code: " + conn.getResponseCode()); } - protected static void unexpectedResponse(Status status) throws ServalDInterfaceException + protected static ServalDInterfaceException unexpectedResponse(Status status) { - throw new ServalDInterfaceException( + return new ServalDInterfaceException( "unexpected Rhizome failure, \"" + status.http_status_message + "\"" + (status.bundle_status_code == null ? "" : ", " + status.bundle_status_code) + (status.bundle_status_message == null ? "" : " \"" + status.bundle_status_message + "\"") @@ -95,34 +107,6 @@ public class RhizomeCommon ); } -/* - protected static void throwRestfulResponseExceptions(Status status, URL url) throws RhizomeException, ServalDFailureException - { - if (status.bundle_status_code != null) { - switch (status.bundle_status_code) { - case ERROR: - throw new ServalDFailureException("received rhizome_bundle_status_code=ERROR(-1) from " + url); - case NEW: - throw new RhizomeManifestNotFoundException(url); - case SAME: - throw new RhizomeManifestAlreadyStoredException(url); - case DUPLICATE: - throw new RhizomeDuplicateBundleException(url); - case OLD: - throw new RhizomeOutdatedBundleException(url); - case NO_ROOM: - throw new RhizomeStoreFullException(url); - case INVALID: - throw new RhizomeInvalidManifestException(url); - case FAKE: - throw new RhizomeFakeManifestException(url); - case INCONSISTENT: - throw new RhizomeInconsistencyException(url); - } - } - } -*/ - protected static JSONTokeniser receiveRestfulResponse(HttpURLConnection conn, int expected_response_code) throws IOException, ServalDInterfaceException, RhizomeException { int[] expected_response_codes = { expected_response_code }; @@ -227,6 +211,7 @@ public class RhizomeCommon try { dumpHeaders(conn, System.err); decodeHeaderBundleStatus(status, conn); + dumpStatus(status, System.err); switch (status.bundle_status_code) { case NEW: return null; @@ -235,7 +220,7 @@ public class RhizomeCommon throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType()); RhizomeManifest manifest = RhizomeManifest.fromTextFormat(status.input_stream); BundleExtra extra = bundleExtraFromHeaders(conn); - return new RhizomeManifestBundle(manifest, extra.insertTime, extra.author, extra.secret); + return new RhizomeManifestBundle(manifest, extra.rowId, extra.insertTime, extra.author, extra.secret); case ERROR: throw new ServalDFailureException("received rhizome_bundle_status_code=ERROR(-1) from " + conn.getURL()); } @@ -247,8 +232,7 @@ public class RhizomeCommon if (status.input_stream != null) status.input_stream.close(); } - unexpectedResponse(status); - return null; + throw unexpectedResponse(status); } public static RhizomePayloadRawBundle rhizomePayloadRaw(ServalDHttpConnectionFactory connector, BundleId bid) @@ -260,6 +244,7 @@ public class RhizomeCommon try { dumpHeaders(conn, System.err); decodeHeaderBundleStatus(status, conn); + dumpStatus(status, System.err); switch (status.bundle_status_code) { case ERROR: throw new ServalDFailureException("received rhizome_bundle_status_code=ERROR(-1) from " + conn.getURL()); @@ -285,7 +270,7 @@ public class RhizomeCommon throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType()); RhizomeManifest manifest = manifestFromHeaders(conn); BundleExtra extra = bundleExtraFromHeaders(conn); - RhizomePayloadRawBundle ret = new RhizomePayloadRawBundle(manifest, status.input_stream, extra.insertTime, extra.author, extra.secret); + RhizomePayloadRawBundle ret = new RhizomePayloadRawBundle(manifest, status.input_stream, extra.rowId, extra.insertTime, extra.author, extra.secret); status.input_stream = null; // don't close when we return return ret; } @@ -296,8 +281,7 @@ public class RhizomeCommon if (status.input_stream != null) status.input_stream.close(); } - unexpectedResponse(status); - return null; + throw unexpectedResponse(status); } public static RhizomePayloadBundle rhizomePayload(ServalDHttpConnectionFactory connector, BundleId bid) @@ -309,6 +293,7 @@ public class RhizomeCommon try { dumpHeaders(conn, System.err); decodeHeaderBundleStatus(status, conn); + dumpStatus(status, System.err); switch (status.bundle_status_code) { case ERROR: throw new ServalDFailureException("received rhizome_bundle_status_code=ERROR(-1) from " + conn.getURL()); @@ -336,7 +321,7 @@ public class RhizomeCommon throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType()); RhizomeManifest manifest = manifestFromHeaders(conn); BundleExtra extra = bundleExtraFromHeaders(conn); - RhizomePayloadBundle ret = new RhizomePayloadBundle(manifest, status.input_stream, extra.insertTime, extra.author, extra.secret); + RhizomePayloadBundle ret = new RhizomePayloadBundle(manifest, status.input_stream, extra.rowId, extra.insertTime, extra.author, extra.secret); status.input_stream = null; // don't close when we return return ret; } @@ -347,8 +332,134 @@ public class RhizomeCommon if (status.input_stream != null) status.input_stream.close(); } - unexpectedResponse(status); - return null; + throw unexpectedResponse(status); + } + + public static RhizomeInsertBundle rhizomeInsert(ServalDHttpConnectionFactory connector, + SubscriberId author, + RhizomeIncompleteManifest manifest) + throws ServalDInterfaceException, + IOException, + RhizomeInvalidManifestException, + RhizomeFakeManifestException, + RhizomeInconsistencyException, + RhizomeReadOnlyException, + RhizomeEncryptionException + { + return rhizomeInsert(connector, author, manifest, null, null); + } + + public static RhizomeInsertBundle rhizomeInsert(ServalDHttpConnectionFactory connector, + SubscriberId author, + RhizomeIncompleteManifest manifest, + InputStream payloadStream, + String fileName) + throws ServalDInterfaceException, + IOException, + RhizomeInvalidManifestException, + RhizomeFakeManifestException, + RhizomeInconsistencyException, + RhizomeReadOnlyException, + RhizomeEncryptionException + { + HttpURLConnection conn = connector.newServalDHttpConnection("/restful/rhizome/insert"); + String boundary = Long.toHexString(System.currentTimeMillis()); + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); + conn.connect(); + OutputStream ost = conn.getOutputStream(); + PrintStream wr = new PrintStream(ost, false, "US-ASCII"); + if (author != null) { + wr.print("\r\n--" + boundary + "\r\n"); + wr.print("Content-Disposition: form-data; name=\"bundle-author\"\r\n"); + wr.print("\r\n"); + wr.print(author.toHex()); + } + wr.print("\r\n--" + boundary + "\r\n"); + wr.print("Content-Disposition: form-data; name=\"manifest\"\r\n"); + wr.print("Content-Type: rhizome-manifest/text\r\n"); + wr.print("\r\n"); + wr.flush(); + manifest.toTextFormat(ost); + if (payloadStream != null) { + wr.print("\r\n--" + boundary + "\r\n"); + wr.print("Content-Disposition: form-data; name=\"payload\""); + if (fileName != null) { + wr.print("; filename="); + wr.print(quoteString(fileName)); + } + wr.print("\r\n"); + wr.print("Content-Type: application/octet-stream\r\n"); + wr.print("\r\n"); + wr.flush(); + byte[] buffer = new byte[4096]; + int n; + while ((n = payloadStream.read(buffer)) > 0) + ost.write(buffer, 0, n); + } + wr.print("\r\n--" + boundary + "--\r\n"); + wr.close(); + int[] expected_response_codes = { HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_CREATED }; + Status status = RhizomeCommon.receiveResponse(conn, expected_response_codes); + try { + dumpHeaders(conn, System.err); + decodeHeaderPayloadStatus(status, conn); + switch (status.payload_status_code) { + case ERROR: + dumpStatus(status, System.err); + throw new ServalDFailureException("received rhizome_payload_status_code=ERROR(-1) from " + conn.getURL()); + case EMPTY: + case NEW: + case STORED: + decodeHeaderBundleStatus(status, conn); + dumpStatus(status, System.err); + switch (status.bundle_status_code) { + case ERROR: + throw new ServalDFailureException("received rhizome_bundle_status_code=ERROR(-1) from " + conn.getURL()); + case NEW: + case SAME: + case DUPLICATE: + case OLD: + case NO_ROOM: { + if (!conn.getContentType().equals("rhizome-manifest/text")) + throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType()); + RhizomeManifest returned_manifest = RhizomeManifest.fromTextFormat(status.input_stream); + BundleExtra extra = bundleExtraFromHeaders(conn); + return new RhizomeInsertBundle(status.bundle_status_code, returned_manifest, extra.rowId, extra.insertTime, extra.author, extra.secret); + } + case INVALID: + throw new RhizomeInvalidManifestException(conn.getURL()); + case FAKE: + throw new RhizomeFakeManifestException(conn.getURL()); + case INCONSISTENT: + throw new RhizomeInconsistencyException(conn.getURL()); + case READONLY: + throw new RhizomeReadOnlyException(conn.getURL()); + } + break; + case TOO_BIG: + case EVICTED: + dumpStatus(status, System.err); + return null; + case WRONG_SIZE: + case WRONG_HASH: + dumpStatus(status, System.err); + throw new RhizomeInconsistencyException(conn.getURL()); + case CRYPTO_FAIL: + dumpStatus(status, System.err); + throw new RhizomeEncryptionException(conn.getURL()); + } + } + catch (RhizomeManifestParseException e) { + throw new ServalDInterfaceException("malformed manifest from daemon", e); + } + finally { + if (status.input_stream != null) + status.input_stream.close(); + } + dumpStatus(status, System.err); + throw unexpectedResponse(status); } private static void dumpHeaders(HttpURLConnection conn, PrintStream out) @@ -376,7 +487,8 @@ public class RhizomeCommon } private static class BundleExtra { - public long insertTime; + public Long rowId; + public Long insertTime; public SubscriberId author; public BundleSecret secret; } @@ -384,15 +496,35 @@ public class RhizomeCommon private static BundleExtra bundleExtraFromHeaders(HttpURLConnection conn) throws ServalDInterfaceException { BundleExtra extra = new BundleExtra(); - extra.insertTime = headerUnsignedLong(conn, "Serval-Rhizome-Bundle-Inserttime"); - extra.author = header(conn, "Serval-Rhizome-Bundle-Author", SubscriberId.class); - extra.secret = header(conn, "Serval-Rhizome-Bundle-Secret", BundleSecret.class); + extra.rowId = headerUnsignedLongOrNull(conn, "Serval-Rhizome-Bundle-Rowid"); + extra.insertTime = headerUnsignedLongOrNull(conn, "Serval-Rhizome-Bundle-Inserttime"); + extra.author = headerOrNull(conn, "Serval-Rhizome-Bundle-Author", SubscriberId.class); + extra.secret = headerOrNull(conn, "Serval-Rhizome-Bundle-Secret", BundleSecret.class); return extra; } + private static String quoteString(String unquoted) + { + StringBuilder b = new StringBuilder(unquoted.length() + 2); + b.append('"'); + for (int i = 0; i < unquoted.length(); ++i) { + char c = unquoted.charAt(i); + if (c == '"' || c == '\\') + b.append('\\'); + b.append(c); + } + b.append('"'); + return b.toString(); + } + + private static String headerStringOrNull(HttpURLConnection conn, String header) throws ServalDInterfaceException + { + return conn.getHeaderField(header); + } + private static String headerString(HttpURLConnection conn, String header) throws ServalDInterfaceException { - String str = conn.getHeaderField(header); + String str = headerStringOrNull(conn, header); if (str == null) throw new ServalDInterfaceException("missing header field: " + header); return str; diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeEncryptionException.java b/java/org/servalproject/servaldna/rhizome/RhizomeEncryptionException.java new file mode 100644 index 00000000..d91b1ac6 --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeEncryptionException.java @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software 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 3 of the License, or + * (at your option) any later version. + * + * This source code 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 source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.rhizome; + +import java.net.URL; + +/** + * Thrown when a Rhizome API method is asked to encrypt a payload without possessing the necessary + * author or sender secret (not in keyring, or identity not unlocked) and without possessing the + * bundle secret. + * + * @author Andrew Bettison + */ +public class RhizomeEncryptionException extends RhizomeException +{ + public RhizomeEncryptionException(URL url) { + super("cannot encrypt payload", url); + } + +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeException.java b/java/org/servalproject/servaldna/rhizome/RhizomeException.java index 2b895791..8f5da3a6 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeException.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeException.java @@ -32,6 +32,11 @@ public abstract class RhizomeException extends Exception { public final URL url; + public RhizomeException(String message) { + super(message); + this.url = null; + } + public RhizomeException(String message, URL url) { super(message + "; " + url); this.url = url; diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeIncompleteManifest.java b/java/org/servalproject/servaldna/rhizome/RhizomeIncompleteManifest.java new file mode 100644 index 00000000..5dbc50a1 --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeIncompleteManifest.java @@ -0,0 +1,267 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software 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 3 of the License, or + * (at your option) any later version. + * + * This source code 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 source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.rhizome; + +import java.util.Map; +import java.util.HashMap; +import java.util.HashSet; +import java.io.UnsupportedEncodingException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import org.servalproject.servaldna.AbstractId; +import org.servalproject.servaldna.SubscriberId; +import org.servalproject.servaldna.BundleId; +import org.servalproject.servaldna.FileHash; +import org.servalproject.servaldna.BundleKey; + +public class RhizomeIncompleteManifest { + + public BundleId id; + public Long version; + public Long filesize; + public FileHash filehash; + public SubscriberId sender; + public SubscriberId recipient; + public BundleKey BK; + public Long tail; + public Integer crypt; + public Long date; + public String service; + public String name; + private HashMap extraFields; + + public RhizomeIncompleteManifest() + { + this.extraFields = new HashMap(); + } + + @SuppressWarnings("unchecked") + public RhizomeIncompleteManifest(RhizomeManifest m) + { + this.id = m.id; + this.version = m.version; + this.filesize = m.filesize; + this.filehash = m.filehash; + this.sender = m.sender; + this.recipient = m.recipient; + this.BK = m.BK; + this.crypt = m.crypt; + this.tail = m.tail; + this.date = m.date; + this.service = m.service; + this.name = m.name; + this.extraFields = (HashMap) m.extraFields.clone(); // unchecked cast + } + + /** Return the Rhizome manifest in its text format representation. + * + * @author Andrew Bettison + */ + public void toTextFormat(OutputStream os) throws IOException + { + OutputStreamWriter osw = new OutputStreamWriter(os, "US-ASCII"); + if (id != null) + osw.write("id=" + id.toHex() + "\n"); + if (version != null) + osw.write("version=" + version + "\n"); + if (filesize != null) + osw.write("filesize=" + filesize + "\n"); + if (filehash != null) + osw.write("filehash=" + filehash.toHex() + "\n"); + if (sender != null) + osw.write("sender=" + sender.toHex() + "\n"); + if (recipient != null) + osw.write("recipient=" + recipient.toHex() + "\n"); + if (BK != null) + osw.write("BK=" + BK.toHex() + "\n"); + if (crypt != null) + osw.write("crypt=" + crypt + "\n"); + if (tail != null) + osw.write("tail=" + tail + "\n"); + if (date != null) + osw.write("date=" + date + "\n"); + if (service != null) + osw.write("service=" + service + "\n"); + if (name != null) + osw.write("name=" + name + "\n"); + for (Map.Entry e: extraFields.entrySet()) + osw.write(e.getKey() + "=" + e.getValue() + "\n"); + osw.flush(); + } + + /** Construct a Rhizome manifest from its text format representation. + * + * @author Andrew Bettison + */ + public static RhizomeIncompleteManifest fromTextFormat(byte[] bytes) throws RhizomeManifestParseException + { + RhizomeIncompleteManifest m = new RhizomeIncompleteManifest(); + try { + m.parseTextFormat(new ByteArrayInputStream(bytes, 0, bytes.length)); + } + catch (IOException e) { + } + return m; + } + + /** Construct a Rhizome manifest from its text format representation. + * + * @author Andrew Bettison + */ + public static RhizomeIncompleteManifest fromTextFormat(byte[] bytes, int off, int len) throws RhizomeManifestParseException + { + RhizomeIncompleteManifest m = new RhizomeIncompleteManifest(); + try { + m.parseTextFormat(new ByteArrayInputStream(bytes, off, len)); + } + catch (IOException e) { + } + return m; + } + + /** Convenience method: construct a Rhizome manifest from all the bytes read from a given + * InputStream. + * + * @author Andrew Bettison + */ + static public RhizomeIncompleteManifest fromTextFormat(InputStream in) throws IOException, RhizomeManifestParseException + { + RhizomeIncompleteManifest m = new RhizomeIncompleteManifest(); + m.parseTextFormat(in); + return m; + } + + /** Fill in manifest fields from a text format representation. + * + * @author Andrew Bettison + */ + public void parseTextFormat(InputStream in) throws IOException, RhizomeManifestParseException + { + try { + InputStreamReader inr = new InputStreamReader(in, "US-ASCII"); + int pos = 0; + int lnum = 1; + int eq = -1; + StringBuilder line = new StringBuilder(); + while (true) { + int c = inr.read(); + if (c != -1 && c != '\n') { + if (eq == -1 && c == '=') + eq = line.length(); + line.append((char)c); + } + else if (line.length() == 0) + break; + else if (eq < 1) + throw new RhizomeManifestParseException("malformed (missing '=') at line " + lnum + ": " + line); + else { + String fieldName = line.substring(0, eq); + String fieldValue = line.substring(eq + 1); + if (!isFieldNameFirstChar(fieldName.charAt(0))) + throw new RhizomeManifestParseException("invalid field name at line " + lnum + ": " + line); + for (int i = 1; i < fieldName.length(); ++i) + if (!isFieldNameChar(fieldName.charAt(i))) + throw new RhizomeManifestParseException("invalid field name at line " + lnum + ": " + line); + try { + if (fieldName.equals("id")) + this.id = parseField(this.id, new BundleId(fieldValue)); + else if (fieldName.equals("version")) + this.version = parseField(this.version, parseUnsignedLong(fieldValue)); + else if (fieldName.equals("filesize")) + this.filesize = parseField(this.filesize, parseUnsignedLong(fieldValue)); + else if (fieldName.equals("filehash")) + this.filehash = parseField(this.filehash, new FileHash(fieldValue)); + else if (fieldName.equals("sender")) + this.sender = parseField(this.sender, new SubscriberId(fieldValue)); + else if (fieldName.equals("recipient")) + this.recipient = parseField(this.recipient, new SubscriberId(fieldValue)); + else if (fieldName.equals("BK")) + this.BK = parseField(this.BK, new BundleKey(fieldValue)); + else if (fieldName.equals("crypt")) + this.crypt = parseField(this.crypt, Integer.parseInt(fieldValue)); + else if (fieldName.equals("tail")) + this.tail = parseField(this.tail, parseUnsignedLong(fieldValue)); + else if (fieldName.equals("date")) + this.date = parseField(this.date, parseUnsignedLong(fieldValue)); + else if (fieldName.equals("service")) + this.service = parseField(this.service, fieldValue); + else if (fieldName.equals("name")) + this.name = parseField(this.name, fieldValue); + else if (this.extraFields.containsKey(fieldName)) + throw new RhizomeManifestParseException("duplicate field"); + else + this.extraFields.put(fieldName, fieldValue); + } + catch (RhizomeManifestParseException e) { + throw new RhizomeManifestParseException(e.getMessage() + " at line " + lnum + ": " + line); + } + catch (AbstractId.InvalidHexException e) { + throw new RhizomeManifestParseException("invalid value at line " + lnum + ": " + line, e); + } + catch (NumberFormatException e) { + throw new RhizomeManifestParseException("invalid value at line " + lnum + ": " + line, e); + } + line.setLength(0); + eq = -1; + ++lnum; + } + } + if (line.length() > 0) + throw new RhizomeManifestParseException("malformed (missing newline) at line " + lnum + ": " + line); + } + catch (UnsupportedEncodingException e) { + throw new RhizomeManifestParseException(e); + } + } + + static private T parseField(T currentValue, T newValue) throws RhizomeManifestParseException + { + if (currentValue == null) + return newValue; + if (!currentValue.equals(newValue)) + throw new RhizomeManifestParseException("duplicate field"); + return currentValue; + } + + private static boolean isFieldNameFirstChar(char c) + { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); + } + + private static boolean isFieldNameChar(char c) + { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'); + } + + private static Long parseUnsignedLong(String text) throws NumberFormatException + { + Long value = Long.valueOf(text); + if (value < 0) + throw new NumberFormatException("negative value not allowed"); + return value; + } + +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeInsertBundle.java b/java/org/servalproject/servaldna/rhizome/RhizomeInsertBundle.java new file mode 100644 index 00000000..c3b053ec --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeInsertBundle.java @@ -0,0 +1,42 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software 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 3 of the License, or + * (at your option) any later version. + * + * This source code 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 source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.rhizome; + +import org.servalproject.servaldna.SubscriberId; +import org.servalproject.servaldna.BundleSecret; +import org.servalproject.servaldna.ServalDInterfaceException; + +public class RhizomeInsertBundle extends RhizomeManifestBundle { + + public final RhizomeBundleStatus status; + + protected RhizomeInsertBundle(RhizomeBundleStatus status, + RhizomeManifest manifest, + Long rowId, + Long insertTime, + SubscriberId author, + BundleSecret secret) + { + super(manifest, rowId, insertTime, author, secret); + this.status = status; + } + +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeInvalidManifestException.java b/java/org/servalproject/servaldna/rhizome/RhizomeInvalidManifestException.java index f76bc01b..f1966218 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeInvalidManifestException.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeInvalidManifestException.java @@ -23,9 +23,9 @@ package org.servalproject.servaldna.rhizome; import java.net.URL; /** - * Thrown when a Rhizome API method is passed an invalid manifest. This is not an error within the - * Serval DNA interface, so it is not a subclass of ServalDInterfaceException. The programmer must - * explicitly deal with it instead of just absorbing it as an interface malfunction. + * Thrown when the Rhizome API rejects a caller-supplied manifest as invalid. This error does not + * originate from the Serval DNA interface, so it is not a subclass of ServalDInterfaceException. + * The programmer must deal with it, and not treat it as an interface malfunction. * * @author Andrew Bettison */ @@ -35,4 +35,8 @@ public class RhizomeInvalidManifestException extends RhizomeException super("invalid manifest", url); } + public RhizomeInvalidManifestException(RhizomeIncompleteManifest manifest) { + super("invalid manifest"); + } + } diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeListBundle.java b/java/org/servalproject/servaldna/rhizome/RhizomeListBundle.java index c5247fcd..22fec2e5 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeListBundle.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeListBundle.java @@ -39,7 +39,6 @@ public class RhizomeListBundle { long insertTime, SubscriberId author, int fromHere) - { this.manifest = manifest; this.rowNumber = rowNumber; diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeManifest.java b/java/org/servalproject/servaldna/rhizome/RhizomeManifest.java index c5fad0ac..08068f46 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeManifest.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeManifest.java @@ -26,8 +26,10 @@ import java.util.HashSet; import java.io.UnsupportedEncodingException; import java.io.IOException; import java.io.InputStream; -import java.io.ByteArrayOutputStream; +import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import org.servalproject.servaldna.AbstractId; import org.servalproject.servaldna.SubscriberId; import org.servalproject.servaldna.BundleId; @@ -56,7 +58,7 @@ public class RhizomeManifest { public final String service; public final String name; - private HashMap extraFields; + protected HashMap extraFields; private byte[] signatureBlock; private byte[] textFormat; @@ -96,59 +98,51 @@ public class RhizomeManifest { this.textFormat = null; } - /** Return the Rhizome manifest in its text format representation. + protected RhizomeManifest(RhizomeIncompleteManifest m) + { + this(m.id, m.version, m.filesize, m.filehash, m.sender, m.recipient, m.BK, m.crypt, m.tail, m.date, m.service, m.name); + } + + /** Return the Rhizome manifest in its text format representation, with the signature block at + * the end if present. * * @author Andrew Bettison */ public byte[] toTextFormat() throws RhizomeManifestSizeException { - if (textFormat == null) { + if (this.textFormat == null) { try { ByteArrayOutputStream os = new ByteArrayOutputStream(); - OutputStreamWriter osw = new OutputStreamWriter(os, "US-ASCII"); - osw.write("id=" + id.toHex() + "\n"); - osw.write("version=" + version + "\n"); - osw.write("filesize=" + filesize + "\n"); - if (filehash != null) - osw.write("filehash=" + filehash.toHex() + "\n"); - if (sender != null) - osw.write("sender=" + sender.toHex() + "\n"); - if (recipient != null) - osw.write("recipient=" + recipient.toHex() + "\n"); - if (BK != null) - osw.write("BK=" + BK.toHex() + "\n"); - if (crypt != null) - osw.write("crypt=" + crypt + "\n"); - if (tail != null) - osw.write("tail=" + tail + "\n"); - if (date != null) - osw.write("date=" + date + "\n"); - if (service != null) - osw.write("service=" + service + "\n"); - if (name != null) - osw.write("name=" + name + "\n"); - for (Map.Entry e: extraFields.entrySet()) - osw.write(e.getKey() + "=" + e.getValue() + "\n"); - osw.flush(); - if (signatureBlock != null) { - os.write(0); - os.write(signatureBlock); - } - osw.close(); + toTextFormat(os); + os.close(); if (os.size() > TEXT_FORMAT_MAX_SIZE) throw new RhizomeManifestSizeException("manifest text format overflow", os.size(), TEXT_FORMAT_MAX_SIZE); - textFormat = os.toByteArray(); + this.textFormat = os.toByteArray(); } catch (IOException e) { // should not happen with ByteArrayOutputStream return new byte[0]; } } - byte[] ret = new byte[textFormat.length]; - System.arraycopy(textFormat, 0, ret, 0, ret.length); + byte[] ret = new byte[this.textFormat.length]; + System.arraycopy(this.textFormat, 0, ret, 0, ret.length); return ret; } + /** Write the Rhizome manifest in its text format representation to the given output stream, + * with the signature block at the end if present. + * + * @author Andrew Bettison + */ + public void toTextFormat(OutputStream os) throws IOException + { + new RhizomeIncompleteManifest(this).toTextFormat(os); + if (this.signatureBlock != null) { + os.write(0); + os.write(this.signatureBlock); + } + } + /** Construct a Rhizome manifest from its text format representation. * * @author Andrew Bettison @@ -158,7 +152,8 @@ public class RhizomeManifest { return fromTextFormat(bytes, 0, bytes.length); } - /** Construct a Rhizome manifest from its text format representation. + /** Construct a complete Rhizome manifest from its text format representation, including a + * trailing signature block. * * @author Andrew Bettison */ @@ -175,95 +170,23 @@ public class RhizomeManifest { break; } } - String text; + RhizomeIncompleteManifest im = new RhizomeIncompleteManifest(); try { - text = new String(bytes, off, proplen, "US-ASCII"); + im.parseTextFormat(new ByteArrayInputStream(bytes, off, proplen)); } - catch (UnsupportedEncodingException e) { - throw new RhizomeManifestParseException(e); + catch (IOException e) { } - BundleId id = null; - Long version = null; - Long filesize = null; - FileHash filehash = null; - SubscriberId sender = null; - SubscriberId recipient = null; - BundleKey BK = null; - Integer crypt = null; - Long tail = null; - Long date = null; - String service = null; - String name = null; - HashMap extras = new HashMap(); - int pos = 0; - int lnum = 1; - while (pos < text.length()) { - int nl = text.indexOf('\n', pos); - if (nl == -1) - nl = text.length(); - int field = pos; - if (!isFieldNameFirstChar(text.charAt(field))) - throw new RhizomeManifestParseException("invalid field name at line " + lnum + ": " + text.substring(pos, nl - pos)); - ++field; - while (isFieldNameChar(text.charAt(field))) - ++field; - assert field < nl; - if (text.charAt(field) != '=') - throw new RhizomeManifestParseException("invalid field name at line " + lnum + ": " + text.substring(pos, nl - pos)); - String fieldName = text.substring(pos, field); - String fieldValue = text.substring(field + 1, nl); - HashSet fieldNames = new HashSet(50); - try { - if (fieldNames.contains(fieldName)) - throw new RhizomeManifestParseException("duplicate field at line " + lnum + ": " + text.substring(pos, nl - pos)); - fieldNames.add(fieldName); - if (fieldName.equals("id")) - id = new BundleId(fieldValue); - else if (fieldName.equals("version")) - version = parseUnsignedLong(fieldValue); - else if (fieldName.equals("filesize")) - filesize = parseUnsignedLong(fieldValue); - else if (fieldName.equals("filehash")) - filehash = new FileHash(fieldValue); - else if (fieldName.equals("sender")) - sender = new SubscriberId(fieldValue); - else if (fieldName.equals("recipient")) - recipient = new SubscriberId(fieldValue); - else if (fieldName.equals("BK")) - BK = new BundleKey(fieldValue); - else if (fieldName.equals("crypt")) - crypt = Integer.parseInt(fieldValue); - else if (fieldName.equals("tail")) - tail = parseUnsignedLong(fieldValue); - else if (fieldName.equals("date")) - date = parseUnsignedLong(fieldValue); - else if (fieldName.equals("service")) - service = fieldValue; - else if (fieldName.equals("name")) - name = fieldValue; - else - extras.put(fieldName, fieldValue); - } - catch (AbstractId.InvalidHexException e) { - throw new RhizomeManifestParseException("invalid value at line " + lnum + ": " + text.substring(pos, nl - pos), e); - } - catch (NumberFormatException e) { - throw new RhizomeManifestParseException("invalid value at line " + lnum + ": " + text.substring(pos, nl - pos), e); - } - pos = nl + 1; - } - if (id == null) + if (im.id == null) throw new RhizomeManifestParseException("missing 'id' field"); - if (version == null) + if (im.version == null) throw new RhizomeManifestParseException("missing 'version' field"); - if (filesize == null) + if (im.filesize == null) throw new RhizomeManifestParseException("missing 'filesize' field"); - if (filesize != 0 && filehash == null) + if (im.filesize != 0 && im.filehash == null) throw new RhizomeManifestParseException("missing 'filehash' field"); - else if (filesize == 0 && filehash != null) + else if (im.filesize == 0 && im.filehash != null) throw new RhizomeManifestParseException("spurious 'filehash' field"); - RhizomeManifest m = new RhizomeManifest(id, version, filesize, filehash, sender, recipient, BK, crypt, tail, date, service, name); - m.extraFields = extras; + RhizomeManifest m = new RhizomeManifest(im); m.signatureBlock = sigblock; m.textFormat = new byte[len]; System.arraycopy(bytes, off, m.textFormat, 0, m.textFormat.length); diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeManifestBundle.java b/java/org/servalproject/servaldna/rhizome/RhizomeManifestBundle.java index 452aa73d..0d10aeca 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeManifestBundle.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeManifestBundle.java @@ -26,18 +26,20 @@ import org.servalproject.servaldna.ServalDInterfaceException; public class RhizomeManifestBundle { - public final long insertTime; + public final Long insertTime; + public final Long rowId; public final SubscriberId author; public final BundleSecret secret; public final RhizomeManifest manifest; protected RhizomeManifestBundle(RhizomeManifest manifest, - long insertTime, + Long rowId, + Long insertTime, SubscriberId author, BundleSecret secret) - { this.manifest = manifest; + this.rowId = rowId; this.insertTime = insertTime; this.author = author; this.secret = secret; diff --git a/java/org/servalproject/servaldna/rhizome/RhizomePayloadBundle.java b/java/org/servalproject/servaldna/rhizome/RhizomePayloadBundle.java index 590eea0c..a65fd652 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomePayloadBundle.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomePayloadBundle.java @@ -29,19 +29,21 @@ public class RhizomePayloadBundle { public final RhizomeManifest manifest; public final InputStream payloadInputStream; - public final long insertTime; + public final Long rowId; + public final Long insertTime; public final SubscriberId author; public final BundleSecret secret; protected RhizomePayloadBundle(RhizomeManifest manifest, InputStream payloadInputStream, - long insertTime, + Long rowId, + Long insertTime, SubscriberId author, BundleSecret secret) - { this.payloadInputStream = payloadInputStream; this.manifest = manifest; + this.rowId = rowId; this.insertTime = insertTime; this.author = author; this.secret = secret; diff --git a/java/org/servalproject/servaldna/rhizome/RhizomePayloadRawBundle.java b/java/org/servalproject/servaldna/rhizome/RhizomePayloadRawBundle.java index ca00792d..64e081c3 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomePayloadRawBundle.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomePayloadRawBundle.java @@ -29,19 +29,21 @@ public class RhizomePayloadRawBundle { public final RhizomeManifest manifest; public final InputStream rawPayloadInputStream; - public final long insertTime; + public final Long rowId; + public final Long insertTime; public final SubscriberId author; public final BundleSecret secret; protected RhizomePayloadRawBundle(RhizomeManifest manifest, InputStream rawPayloadInputStream, - long insertTime, + Long rowId, + Long insertTime, SubscriberId author, BundleSecret secret) - { this.rawPayloadInputStream = rawPayloadInputStream; this.manifest = manifest; + this.rowId = rowId; this.insertTime = insertTime; this.author = author; this.secret = secret; diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeReadOnlyException.java b/java/org/servalproject/servaldna/rhizome/RhizomeReadOnlyException.java new file mode 100644 index 00000000..d910c77c --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeReadOnlyException.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software 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 3 of the License, or + * (at your option) any later version. + * + * This source code 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 source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.rhizome; + +import java.net.URL; + +/** + * Thrown when a Rhizome API method is passed a manifest which is inconsistent with a supplied + * payload. I.e., filesize or filehash does not match. + * + * @author Andrew Bettison + */ +public class RhizomeReadOnlyException extends RhizomeException +{ + public RhizomeReadOnlyException(URL url) { + super("bundle cannot be modified", url); + } + +} diff --git a/java/org/servalproject/test/Rhizome.java b/java/org/servalproject/test/Rhizome.java index 9c9412cf..0def1887 100644 --- a/java/org/servalproject/test/Rhizome.java +++ b/java/org/servalproject/test/Rhizome.java @@ -20,36 +20,42 @@ package org.servalproject.test; -import java.io.IOException; +import java.io.File; import java.io.InputStream; import java.io.OutputStream; +import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.IOException; import org.servalproject.servaldna.ServalDClient; import org.servalproject.servaldna.ServalDInterfaceException; import org.servalproject.servaldna.ServerControl; import org.servalproject.servaldna.BundleId; +import org.servalproject.servaldna.SubscriberId; import org.servalproject.servaldna.rhizome.RhizomeManifest; +import org.servalproject.servaldna.rhizome.RhizomeIncompleteManifest; import org.servalproject.servaldna.rhizome.RhizomeListBundle; import org.servalproject.servaldna.rhizome.RhizomeBundleList; import org.servalproject.servaldna.rhizome.RhizomeManifestBundle; import org.servalproject.servaldna.rhizome.RhizomePayloadRawBundle; import org.servalproject.servaldna.rhizome.RhizomePayloadBundle; +import org.servalproject.servaldna.rhizome.RhizomeInsertBundle; import org.servalproject.servaldna.rhizome.RhizomeException; +import org.servalproject.servaldna.rhizome.RhizomeManifestParseException; public class Rhizome { static String manifestFields(RhizomeManifest manifest, String sep) { - return "id=" + manifest.id + sep + - "version=" + manifest.version + sep + - "filesize=" + manifest.filesize + sep + - "filehash=" + manifest.filehash + sep + - "sender=" + manifest.sender + sep + - "recipient=" + manifest.recipient + sep + - "date=" + manifest.date + sep + - "service=" + manifest.service + sep + - "name=" + manifest.name + sep + - "BK=" + manifest.BK; + return "id=" + manifest.id + + sep + "version=" + manifest.version + + sep + "filesize=" + manifest.filesize + + (manifest.filesize != 0 ? sep + "filehash=" + manifest.filehash : "") + + (manifest.sender != null ? sep + "sender=" + manifest.sender : "") + + (manifest.recipient != null ? sep + "recipient=" + manifest.recipient : "") + + (manifest.date != null ? sep + "date=" + manifest.date : "") + + (manifest.service != null ? sep + "service=" + manifest.service : "") + + (manifest.BK != null ? sep + "BK=" + manifest.BK : "") + + (manifest.name != null ? sep + "name=" + manifest.name : ""); } static void rhizome_list() throws ServalDInterfaceException, IOException, InterruptedException @@ -61,8 +67,8 @@ public class Rhizome { RhizomeListBundle bundle; while ((bundle = list.nextBundle()) != null) { System.out.println( - "_rowId=" + bundle.rowId + - ", _token=" + bundle.token + + "_token=" + bundle.token + + ", _rowId=" + bundle.rowId + ", _insertTime=" + bundle.insertTime + ", _author=" + bundle.author + ", _fromHere=" + bundle.fromHere + @@ -89,9 +95,10 @@ public class Rhizome { System.out.println("not found"); else { System.out.println( - "_insertTime=" + bundle.insertTime + "\n" + - "_author=" + bundle.author + "\n" + - "_secret=" + bundle.secret + "\n" + + (bundle.rowId == null ? "" : "_rowId=" + bundle.rowId + "\n") + + (bundle.insertTime == null ? "" : "_insertTime=" + bundle.insertTime + "\n") + + (bundle.author == null ? "" : "_author=" + bundle.author + "\n") + + (bundle.secret == null ? "" : "_secret=" + bundle.secret + "\n") + manifestFields(bundle.manifest, "\n") + "\n" ); FileOutputStream out = new FileOutputStream(dstpath); @@ -128,9 +135,10 @@ public class Rhizome { out = null; } System.out.println( - "_insertTime=" + bundle.insertTime + "\n" + - "_author=" + bundle.author + "\n" + - "_secret=" + bundle.secret + "\n" + + (bundle.rowId == null ? "" : "_rowId=" + bundle.rowId + "\n") + + (bundle.insertTime == null ? "" : "_insertTime=" + bundle.insertTime + "\n") + + (bundle.author == null ? "" : "_author=" + bundle.author + "\n") + + (bundle.secret == null ? "" : "_secret=" + bundle.secret + "\n") + manifestFields(bundle.manifest, "\n") + "\n" ); } @@ -168,9 +176,10 @@ public class Rhizome { out = null; } System.out.println( - "_insertTime=" + bundle.insertTime + "\n" + - "_author=" + bundle.author + "\n" + - "_secret=" + bundle.secret + "\n" + + (bundle.rowId == null ? "" : "_rowId=" + bundle.rowId + "\n") + + (bundle.insertTime == null ? "" : "_insertTime=" + bundle.insertTime + "\n") + + (bundle.author == null ? "" : "_author=" + bundle.author + "\n") + + (bundle.secret == null ? "" : "_secret=" + bundle.secret + "\n") + manifestFields(bundle.manifest, "\n") + "\n" ); } @@ -185,6 +194,43 @@ public class Rhizome { System.exit(0); } + static void rhizome_insert(String author, String manifestpath, String payloadPath, String manifestoutpath, String payloadName) + throws ServalDInterfaceException, IOException, InterruptedException, SubscriberId.InvalidHexException + { + ServalDClient client = new ServerControl().getRestfulClient(); + try { + RhizomeIncompleteManifest manifest = RhizomeIncompleteManifest.fromTextFormat(new FileInputStream(manifestpath)); + RhizomeInsertBundle bundle; + SubscriberId authorSid = author == null || author.length() == 0 ? null : new SubscriberId(author); + if (payloadName == null || payloadName.length() == 0) + payloadName = new File(payloadPath).getName(); + if (payloadPath == null || payloadPath.length() == 0) + bundle = client.rhizomeInsert(authorSid, manifest); + else + bundle = client.rhizomeInsert(authorSid, manifest, new FileInputStream(payloadPath), payloadName); + System.out.println( + "_status=" + bundle.status + "\n" + + (bundle.rowId == null ? "" : "_rowId=" + bundle.rowId + "\n") + + (bundle.insertTime == null ? "" : "_insertTime=" + bundle.insertTime + "\n") + + (bundle.author == null ? "" : "_author=" + bundle.author + "\n") + + (bundle.secret == null ? "" : "_secret=" + bundle.secret + "\n") + + manifestFields(bundle.manifest, "\n") + "\n" + ); + if (manifestoutpath != null && manifestoutpath.length() != 0) { + FileOutputStream out = new FileOutputStream(manifestoutpath); + out.write(bundle.manifestText()); + out.close(); + } + } + catch (RhizomeManifestParseException e) { + System.out.println(e.toString()); + } + catch (RhizomeException e) { + System.out.println(e.toString()); + } + System.exit(0); + } + public static void main(String... args) { if (args.length < 1) @@ -199,6 +245,13 @@ public class Rhizome { rhizome_payload_raw(new BundleId(args[1]), args[2]); else if (methodName.equals("rhizome-payload-decrypted")) rhizome_payload_decrypted(new BundleId(args[1]), args[2]); + else if (methodName.equals("rhizome-insert")) + rhizome_insert( args[1], // author SID + args[2], // manifest path + args.length > 3 ? args[3] : null, // payload path + args.length > 4 ? args[4] : null, // manifest out path + args.length > 5 ? args[5] : null // payload name + ); } catch (Exception e) { e.printStackTrace(); System.exit(1); diff --git a/rhizome_restful.c b/rhizome_restful.c index 6b4b298e..67f7e123 100644 --- a/rhizome_restful.c +++ b/rhizome_restful.c @@ -376,16 +376,20 @@ static int insert_make_manifest(httpd_request *r) r->manifest->manifest_all_bytes = r->u.insert.manifest.length; int n = rhizome_manifest_parse(r->manifest); switch (n) { - case -1: - break; case 0: if (!r->manifest->malformed) return 0; // fall through case 1: + rhizome_manifest_free(r->manifest); + r->manifest = NULL; + r->bundle_status = RHIZOME_BUNDLE_STATUS_INVALID; return http_request_rhizome_response(r, 403, "Malformed manifest", NULL); default: WHYF("rhizome_manifest_parse() returned %d", n); + // fall through + case -1: + r->bundle_status = RHIZOME_BUNDLE_STATUS_ERROR; break; } } @@ -516,6 +520,8 @@ static int insert_mime_part_end(struct http_request *hr) } else if (r->u.insert.current_part == PART_MANIFEST) { r->u.insert.received_manifest = 1; + if (config.debug.rhizome) + DEBUGF("received %s = %s", PART_MANIFEST, alloca_toprint(-1, r->u.insert.manifest.buffer, r->u.insert.manifest.length)); int result = insert_make_manifest(r); if (result) return result; @@ -564,14 +570,14 @@ static int restful_rhizome_insert_end(struct http_request *hr) assert(r->manifest != NULL); assert(r->u.insert.write.file_length != RHIZOME_SIZE_UNSET); int status_valid = 0; + if (config.debug.rhizome) + DEBUGF("r->payload_status=%d", r->payload_status); switch (r->payload_status) { case RHIZOME_PAYLOAD_STATUS_NEW: - status_valid = 1; if (r->manifest->filesize == RHIZOME_SIZE_UNSET) rhizome_manifest_set_filesize(r->manifest, r->u.insert.write.file_length); // fall through case RHIZOME_PAYLOAD_STATUS_STORED: - status_valid = 1; // TODO: check that stored hash matches received payload's hash // fall through case RHIZOME_PAYLOAD_STATUS_EMPTY: @@ -579,18 +585,25 @@ static int restful_rhizome_insert_end(struct http_request *hr) assert(r->manifest->filesize != RHIZOME_SIZE_UNSET); if (r->u.insert.payload_size == r->manifest->filesize) break; + // fall through case RHIZOME_PAYLOAD_STATUS_WRONG_SIZE: r->payload_status = RHIZOME_PAYLOAD_STATUS_WRONG_SIZE; - status_valid = 1; + r->bundle_status = RHIZOME_BUNDLE_STATUS_INCONSISTENT; { strbuf msg = strbuf_alloca(200); strbuf_sprintf(msg, "Payload size (%"PRIu64") contradicts manifest (filesize=%"PRIu64")", r->u.insert.payload_size, r->manifest->filesize); return http_request_rhizome_response(r, 403, NULL, strbuf_str(msg)); } + case RHIZOME_PAYLOAD_STATUS_WRONG_HASH: + r->bundle_status = RHIZOME_BUNDLE_STATUS_INCONSISTENT; + return http_request_rhizome_response(r, 403, NULL, NULL); + case RHIZOME_PAYLOAD_STATUS_CRYPTO_FAIL: + r->bundle_status = RHIZOME_BUNDLE_STATUS_READONLY; + return http_request_rhizome_response(r, 403, "Missing bundle secret", NULL); case RHIZOME_PAYLOAD_STATUS_TOO_BIG: case RHIZOME_PAYLOAD_STATUS_EVICTED: - case RHIZOME_PAYLOAD_STATUS_WRONG_HASH: - case RHIZOME_PAYLOAD_STATUS_CRYPTO_FAIL: + r->bundle_status = RHIZOME_BUNDLE_STATUS_NO_ROOM; + // fall through case RHIZOME_PAYLOAD_STATUS_ERROR: return http_request_rhizome_response(r, 403, NULL, NULL); } @@ -605,17 +618,24 @@ static int restful_rhizome_insert_end(struct http_request *hr) else assert(cmp_rhizome_filehash_t(&r->u.insert.write.id, &r->manifest->filehash) == 0); } - if (!rhizome_manifest_validate(r->manifest) || r->manifest->malformed) { - http_request_simple_response(&r->http, 403, "Manifest is malformed"); - return 403; + const char *invalid_reason = rhizome_manifest_validate_reason(r->manifest); + if (invalid_reason) { + r->bundle_status = RHIZOME_BUNDLE_STATUS_INVALID; + return http_request_rhizome_response(r, 403, invalid_reason, NULL); + } + if (r->manifest->malformed) { + r->bundle_status = RHIZOME_BUNDLE_STATUS_INVALID; + return http_request_rhizome_response(r, 403, r->manifest->malformed, NULL); } if (!r->manifest->haveSecret) { - http_request_simple_response(&r->http, 403, "Missing bundle secret"); - return 403; + r->bundle_status = RHIZOME_BUNDLE_STATUS_READONLY; + return http_request_rhizome_response(r, 403, "Missing bundle secret", NULL); } rhizome_manifest *mout = NULL; r->bundle_status = rhizome_manifest_finalise(r->manifest, &mout, !r->u.insert.force_new); int result = 500; + if (config.debug.rhizome) + DEBUGF("r->bundle_status=%d", r->bundle_status); switch (r->bundle_status) { case RHIZOME_BUNDLE_STATUS_NEW: if (mout && mout != r->manifest) @@ -639,6 +659,8 @@ static int restful_rhizome_insert_end(struct http_request *hr) case RHIZOME_BUNDLE_STATUS_ERROR: if (mout && mout != r->manifest) rhizome_manifest_free(mout); + rhizome_manifest_free(r->manifest); + r->manifest = NULL; return http_request_rhizome_response(r, 0, NULL, NULL); } if (result == 500) diff --git a/testdefs_rhizome.sh b/testdefs_rhizome.sh index 443cc2fe..02e821f0 100644 --- a/testdefs_rhizome.sh +++ b/testdefs_rhizome.sh @@ -196,7 +196,7 @@ unpack_manifest_for_grep() { re_name=$(escape_grep_basic "${filename##*/}") if [ -e "$manifestname" ]; then re_filesize=$($SED -n -e '/^filesize=/s///p' "$manifestname") - if [ "$filesize" = 0 ]; then + if [ "$re_filesize" = 0 ]; then re_filehash= else re_filehash=$($SED -n -e '/^filehash=/s///p' "$manifestname") @@ -243,10 +243,18 @@ extract_stdout_version() { extract_stdout_keyvalue "$1" version "$rexp_version" } +extract_stdout_author_optional() { + extract_stdout_keyvalue_optional "$1" .author "$rexp_author" +} + extract_stdout_author() { extract_stdout_keyvalue "$1" .author "$rexp_author" } +extract_stdout_secret_optional() { + extract_stdout_keyvalue_optional "$1" .secret "$rexp_bundlesecret" +} + extract_stdout_secret() { extract_stdout_keyvalue "$1" .secret "$rexp_bundlesecret" } diff --git a/tests/rhizomejava b/tests/rhizomejava index 64dc0800..6f45f4ca 100755 --- a/tests/rhizomejava +++ b/tests/rhizomejava @@ -30,12 +30,18 @@ setup() { set_instance +A executeOk_servald config \ set log.console.level debug \ - set debug.httpd on + set debug.httpd on \ + set debug.rhizome on \ + set debug.rhizome_manifest on set_extra_config create_identities 4 start_servald_server } +set_extra_config() { + : +} + teardown() { stop_all_servald_servers kill_all_servald_processes @@ -165,6 +171,7 @@ test_RhizomeManifest() { executeJavaOk org.servalproject.test.Rhizome rhizome-manifest "${BID[$n]}" bundle$n.rhm tfw_cat --stdout --stderr assert_metadata $n + ls -l file$n.manifest bundle$n.rhm tfw_cat -v file$n.manifest -v bundle$n.rhm assert diff file$n.manifest bundle$n.rhm done @@ -275,4 +282,91 @@ test_RhizomePayloadDecryptedForeign() { assertStdoutGrep RhizomeDecryptionException } +doc_RhizomeInsert="Java API insert new Rhizome bundles" +setup_RhizomeInsert() { + setup + for n in 1 2 3 4; do + create_file file$n $((1000 + $n)) + create_file nfile$n $((1100 + $n)) + payload_filename[$n]= + eval author[$n]=\$SIDA$n + service[$n]=file + done + name[1]=elvis + echo "name=elvis" >manifest1 + name[2]=file2 + echo "crypt=1" >manifest2 + name[3]=fintlewoodlewix + payload_filename[3]=fintlewoodlewix + >manifest3 + name[4]= + author[4]= + service[4]=wah + echo -e "service=wah\ncrypt=0" >manifest4 +} +test_RhizomeInsert() { + for n in 1 2 3 4; do + executeJavaOk org.servalproject.test.Rhizome rhizome-insert "${author[$n]}" manifest$n file$n file$n.manifest "${payload_filename[$n]}" + tfw_cat --stdout --stderr -v file$n.manifest + assertStdoutGrep '^_status=NEW$' + replayStdout >stdout-insert + extract_manifest_id BID[$n] stdout-insert + extract_manifest SECRET[$n] stdout-insert _secret "$rexp_bundlesecret" + executeOk_servald rhizome extract bundle "${BID[$n]}" xfile$n.manifest xfile$n + tfw_cat --stdout -v xfile$n.manifest + extract_stdout_rowid ROWID[$n] + extract_stdout_inserttime INSERTTIME[$n] + assertGrep stdout-insert "^_rowId=${ROWID[$n]}\$" + assertGrep stdout-insert "^_insertTime=${INSERTTIME[$n]}\$" + if extract_stdout_author_optional AUTHOR[$n]; then + assertGrep stdout-insert "^_author=${AUTHOR[$n]}\$" + else + assertGrep --matches=0 stdout-insert "^_author=" + fi + assert diff xfile$n.manifest file$n.manifest + assert diff file$n xfile$n + unpack_manifest_for_grep xfile$n + assertGrep stdout-insert "^id=$re_manifestid\$" + assertGrep stdout-insert "^version=$re_version\$" + assertGrep stdout-insert "^filesize=$re_filesize\$" + if [ -n "$re_filehash" ]; then + assertGrep stdout-insert "^filehash=$re_filehash\$" + else + assertGrep --matches=0 stdout-insert "^filehash=" + fi + assertGrep stdout-insert "^date=$re_date\$" + assertGrep stdout-insert "^service=$re_service\$" + if [ -n "${name[$n]}" ]; then + assertGrep stdout-insert "^name=$re_name\$" + assert [ "$re_name" = "${name[$n]}" ] + fi + done + executeOk_servald rhizome list + assert_rhizome_list \ + --fromhere=1 \ + --author=${author[1]} file1 \ + --author=${author[2]} file2 \ + --author=${author[3]} file3 \ + --fromhere=0 \ + --author=${author[4]} file4 + for n in 1 2 3 4; do + $SED -e '/^version=/d;/^date=/d;/^filehash=/d;/^filesize=/d;/^[^a-zA-Z]/,$d' xfile$n.manifest >nmanifest$n + assertGrep nmanifest$n '^id=' + tfw_cat -v nmanifest$n + executeJavaOk org.servalproject.test.Rhizome rhizome-insert '' nmanifest$n nfile$n nfile$n.manifest "nfile$n" + tfw_cat --stdout --stderr -v nfile$n.manifest + if [ -n "${author[$n]}" ]; then + assertStdoutGrep '^_status=NEW$' + assertStdoutGrep "^id=${BID[$n]}\$" + assertStderrGrep --matches=1 "^bundle_status_code=NEW$CR\$" + assertStderrGrep --matches=1 --ignore-case "^bundle_status_message=.*bundle new to store.*$CR\$" + assertStderrGrep --matches=1 "^payload_status_code=NEW$CR\$" + assertStderrGrep --matches=1 --ignore-case "^payload_status_message=.*payload new to store.*$CR\$" + else + assertStdoutGrep RhizomeReadOnlyException + assertStderrGrep --ignore-case "missing bundle secret" + fi + done +} + runTests "$@"