From 93e67ede635930e678ac2714b0436de733a53c3d Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Tue, 1 Jul 2014 21:52:12 +0930 Subject: [PATCH] Rhizome Java API: get manifest Fixes assertion violation in GET /restful/rhizome/.rhm when not found --- .../servalproject/servaldna/BundleSecret.java | 44 +++ .../servaldna/ServalDClient.java | 7 + .../servaldna/rhizome/RhizomeBundle.java | 78 ----- .../servaldna/rhizome/RhizomeBundleList.java | 38 ++- .../servaldna/rhizome/RhizomeCommon.java | 123 ++++++- .../servaldna/rhizome/RhizomeListBundle.java | 53 +++ .../servaldna/rhizome/RhizomeManifest.java | 311 ++++++++++++++++++ .../rhizome/RhizomeManifestBundle.java | 55 ++++ .../RhizomeManifestParseException.java | 42 +++ .../rhizome/RhizomeManifestSizeException.java | 48 +++ java/org/servalproject/test/Rhizome.java | 65 +++- rhizome_restful.c | 8 +- tests/rhizomejava | 57 +++- 13 files changed, 788 insertions(+), 141 deletions(-) create mode 100644 java/org/servalproject/servaldna/BundleSecret.java delete mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeBundle.java create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeListBundle.java create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeManifest.java create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeManifestBundle.java create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeManifestParseException.java create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeManifestSizeException.java diff --git a/java/org/servalproject/servaldna/BundleSecret.java b/java/org/servalproject/servaldna/BundleSecret.java new file mode 100644 index 00000000..76a93567 --- /dev/null +++ b/java/org/servalproject/servaldna/BundleSecret.java @@ -0,0 +1,44 @@ +/** + * 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; + +import java.nio.ByteBuffer; + +public class BundleSecret extends AbstractId { + + @Override + public int getBinarySize() { + return 32; + } + + public BundleSecret(String hex) throws InvalidHexException { + super(hex); + } + + public BundleSecret(ByteBuffer b) throws InvalidBinaryException { + super(b); + } + + public BundleSecret(byte[] binary) throws InvalidBinaryException { + super(binary); + } + +} diff --git a/java/org/servalproject/servaldna/ServalDClient.java b/java/org/servalproject/servaldna/ServalDClient.java index 1b3562ad..1788d2fb 100644 --- a/java/org/servalproject/servaldna/ServalDClient.java +++ b/java/org/servalproject/servaldna/ServalDClient.java @@ -32,10 +32,12 @@ import java.net.URLConnection; import java.net.HttpURLConnection; import org.servalproject.codec.Base64; import org.servalproject.servaldna.SubscriberId; +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.RhizomeBundleList; +import org.servalproject.servaldna.rhizome.RhizomeManifestBundle; import org.servalproject.servaldna.meshms.MeshMSCommon; import org.servalproject.servaldna.meshms.MeshMSConversationList; import org.servalproject.servaldna.meshms.MeshMSMessageList; @@ -67,6 +69,11 @@ public class ServalDClient implements ServalDHttpConnectionFactory return list; } + public RhizomeManifestBundle rhizomeManifest(BundleId bid) throws ServalDInterfaceException, IOException + { + return RhizomeCommon.rhizomeManifest(this, bid); + } + public MeshMSConversationList meshmsListConversations(SubscriberId sid) throws ServalDInterfaceException, IOException, MeshMSException { MeshMSConversationList list = new MeshMSConversationList(this, sid); diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeBundle.java b/java/org/servalproject/servaldna/rhizome/RhizomeBundle.java deleted file mode 100644 index 50acadb6..00000000 --- a/java/org/servalproject/servaldna/rhizome/RhizomeBundle.java +++ /dev/null @@ -1,78 +0,0 @@ -/** - * 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.BundleId; -import org.servalproject.servaldna.SubscriberId; -import org.servalproject.servaldna.FileHash; - -public class RhizomeBundle { - - public final int _rowNumber; - public final int _id; - public final String _token; - public final String service; - public final BundleId id; - public final long version; - public final long date; - public final long _inserttime; - public final SubscriberId _author; - public final int _fromhere; - public final long filesize; - public final FileHash filehash; - public final SubscriberId sender; - public final SubscriberId recipient; - public final String name; - - protected RhizomeBundle(int rowNumber, - int _id, - String _token, - String service, - BundleId id, - long version, - long date, - long _inserttime, - SubscriberId _author, - int _fromhere, - long filesize, - FileHash filehash, - SubscriberId sender, - SubscriberId recipient, - String name) - { - this._rowNumber = rowNumber; - this._id = _id; - this._token = _token; - this.service = service; - this.id = id; - this.version = version; - this.date = date; - this._inserttime = _inserttime; - this._author = _author; - this._fromhere = _fromhere; - this.filesize = filesize; - this.filehash = filehash; - this.sender = sender; - this.recipient = recipient; - this.name = name; - } - -} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeBundleList.java b/java/org/servalproject/servaldna/rhizome/RhizomeBundleList.java index 9ef446d5..47845bca 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeBundleList.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeBundleList.java @@ -89,7 +89,7 @@ public class RhizomeBundleList { } } - public RhizomeBundle nextBundle() throws ServalDInterfaceException, IOException + public RhizomeListBundle nextBundle() throws ServalDInterfaceException, IOException { try { Object tok = json.nextToken(); @@ -103,22 +103,26 @@ public class RhizomeBundleList { else json.pushToken(tok); Map row = table.consumeRowArray(json); - return new RhizomeBundle( - rowCount++, - (int)row.get("_id"), - (String)row.get(".token"), - (String)row.get("service"), - (BundleId)row.get("id"), - (long)row.get("version"), - (long)row.get("date"), - (long)row.get(".inserttime"), - (SubscriberId)row.get(".author"), - (int)row.get(".fromhere"), - (long)row.get("filesize"), - (FileHash)row.get("filehash"), - (SubscriberId)row.get("sender"), - (SubscriberId)row.get("recipient"), - (String)row.get("name")); + return new RhizomeListBundle( + new RhizomeManifest((BundleId)row.get("id"), + (long)row.get("version"), + (long)row.get("filesize"), + (FileHash)row.get("filehash"), + (SubscriberId)row.get("sender"), + (SubscriberId)row.get("recipient"), + null, // BK + null, // crypt + null, // tail + (long)row.get("date"), + (String)row.get("service"), + (String)row.get("name")), + rowCount++, + (int)row.get("_id"), + (String)row.get(".token"), + (long)row.get(".inserttime"), + (SubscriberId)row.get(".author"), + (int)row.get(".fromhere") + ); } catch (JSONInputException e) { throw new ServalDInterfaceException(e); diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java index 7f881e02..4c7b3cd9 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java @@ -20,15 +20,47 @@ package org.servalproject.servaldna.rhizome; +import java.lang.reflect.InvocationTargetException; +import java.util.Map; +import java.util.List; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; -import org.servalproject.servaldna.ServalDInterfaceException; import org.servalproject.json.JSONTokeniser; import org.servalproject.json.JSONInputException; +import org.servalproject.servaldna.BundleId; +import org.servalproject.servaldna.SubscriberId; +import org.servalproject.servaldna.BundleSecret; +import org.servalproject.servaldna.ServalDHttpConnectionFactory; +import org.servalproject.servaldna.ServalDInterfaceException; +import org.servalproject.servaldna.ServalDFailureException; public class RhizomeCommon { + + protected static InputStream receiveResponse(HttpURLConnection conn, int expected_response_code) throws IOException, ServalDInterfaceException + { + int[] expected_response_codes = { expected_response_code }; + return receiveResponse(conn, expected_response_codes); + } + + protected static InputStream receiveResponse(HttpURLConnection conn, int[] expected_response_codes) throws IOException, ServalDInterfaceException + { + for (int code: expected_response_codes) { + if (conn.getResponseCode() == code) + return conn.getInputStream(); + } + if (!conn.getContentType().equals("application/json")) + throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType()); + if (conn.getResponseCode() == HttpURLConnection.HTTP_FORBIDDEN) { + JSONTokeniser json = new JSONTokeniser(new InputStreamReader(conn.getErrorStream(), "US-ASCII")); + Status status = decodeRestfulStatus(json); + throw new ServalDInterfaceException("unexpected Rhizome failure, \"" + status.message + "\""); + } + throw new ServalDInterfaceException("unexpected HTTP response code: " + conn.getResponseCode()); + } + protected static JSONTokeniser receiveRestfulResponse(HttpURLConnection conn, int expected_response_code) throws IOException, ServalDInterfaceException { int[] expected_response_codes = { expected_response_code }; @@ -37,20 +69,10 @@ public class RhizomeCommon protected static JSONTokeniser receiveRestfulResponse(HttpURLConnection conn, int[] expected_response_codes) throws IOException, ServalDInterfaceException { + InputStream in = receiveResponse(conn, expected_response_codes); if (!conn.getContentType().equals("application/json")) throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType()); - if (conn.getResponseCode() == HttpURLConnection.HTTP_FORBIDDEN) { - JSONTokeniser json = new JSONTokeniser(new InputStreamReader(conn.getErrorStream(), "US-ASCII")); - Status status = decodeRestfulStatus(json); - throw new ServalDInterfaceException("unexpected Rhizome failure, \"" + status.message + "\""); - } - for (int code: expected_response_codes) { - if (conn.getResponseCode() == code) { - JSONTokeniser json = new JSONTokeniser(new InputStreamReader(conn.getInputStream(), "US-ASCII")); - return json; - } - } - throw new ServalDInterfaceException("unexpected HTTP response code: " + conn.getResponseCode()); + return new JSONTokeniser(new InputStreamReader(in, "US-ASCII")); } private static class Status { @@ -78,4 +100,79 @@ public class RhizomeCommon } } + public static RhizomeManifestBundle rhizomeManifest(ServalDHttpConnectionFactory connector, BundleId bid) throws IOException, ServalDInterfaceException + { + HttpURLConnection conn = connector.newServalDHttpConnection("/restful/rhizome/" + bid.toHex() + ".rhm"); + conn.connect(); + InputStream in = RhizomeCommon.receiveResponse(conn, HttpURLConnection.HTTP_OK); + if (!conn.getContentType().equals("rhizome-manifest/text")) + throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType()); + RhizomeManifest manifest; + try { + manifest = RhizomeManifest.fromTextFormat(in); + } + catch (RhizomeManifestParseException e) { + throw new ServalDInterfaceException("malformed manifest from daemon", e); + } + finally { + in.close(); + } + Map> headers = conn.getHeaderFields(); + for (Map.Entry> e: headers.entrySet()) { + for (String v: e.getValue()) { + System.err.println("received header " + e.getKey() + ": " + v); + } + } + long insertTime = headerUnsignedLong(conn, "Serval-Rhizome-Bundle-Inserttime"); + SubscriberId author = header(conn, "Serval-Rhizome-Bundle-Author", SubscriberId.class); + BundleSecret secret = header(conn, "Serval-Rhizome-Bundle-Secret", BundleSecret.class); + return new RhizomeManifestBundle(manifest, insertTime, author, secret); + } + + private static String headerString(HttpURLConnection conn, String header) throws ServalDInterfaceException + { + String str = conn.getHeaderField(header); + if (str == null) + throw new ServalDInterfaceException("missing header field: " + header); + return str; + } + + private static int headerInteger(HttpURLConnection conn, String header) throws ServalDInterfaceException + { + String str = headerString(conn, header); + try { + return Integer.parseInt(str); + } + catch (NumberFormatException e) { + } + throw new ServalDInterfaceException("invalid header field: " + header + ": " + str); + } + + private static long headerUnsignedLong(HttpURLConnection conn, String header) throws ServalDInterfaceException + { + String str = headerString(conn, header); + try { + long value = Long.parseLong(str); + if (value >= 0) + return value; + } + catch (NumberFormatException e) { + } + throw new ServalDInterfaceException("invalid header field: " + header + ": " + str); + } + + private static T header(HttpURLConnection conn, String header, Class cls) throws ServalDInterfaceException + { + String str = headerString(conn, header); + try { + return (T) cls.getConstructor(String.class).newInstance(str); + } + catch (InvocationTargetException e) { + throw new ServalDInterfaceException("invalid header field: " + header + ": " + str, e.getTargetException()); + } + catch (Exception e) { + throw new ServalDInterfaceException("invalid header field: " + header + ": " + str, e); + } + } + } diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeListBundle.java b/java/org/servalproject/servaldna/rhizome/RhizomeListBundle.java new file mode 100644 index 00000000..c5247fcd --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeListBundle.java @@ -0,0 +1,53 @@ +/** + * 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; + +public class RhizomeListBundle { + + public final int rowNumber; + public final int rowId; + public final String token; + public final long insertTime; + public final SubscriberId author; + public final int fromHere; + public final RhizomeManifest manifest; + + protected RhizomeListBundle(RhizomeManifest manifest, + int rowNumber, + int rowId, + String token, + long insertTime, + SubscriberId author, + int fromHere) + + { + this.manifest = manifest; + this.rowNumber = rowNumber; + this.rowId = rowId; + this.token = token; + this.insertTime = insertTime; + this.author = author; + this.fromHere = fromHere; + } + +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeManifest.java b/java/org/servalproject/servaldna/rhizome/RhizomeManifest.java new file mode 100644 index 00000000..c5fad0ac --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeManifest.java @@ -0,0 +1,311 @@ +/** + * 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.ByteArrayOutputStream; +import java.io.OutputStreamWriter; +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 RhizomeManifest { + + public final static int TEXT_FORMAT_MAX_SIZE = 8192; + + // Core fields used for routing and expiry (cannot be null) + public final BundleId id; + public final long version; + public final long filesize; + + // Principal fields (can be null) + public final FileHash filehash; // null iff filesize == 0 + public final SubscriberId sender; + public final SubscriberId recipient; + public final BundleKey BK; + public final Long tail; // null iff not a journal + public final Integer crypt; + + // Optional fields (all can be null) + public final Long date; // can be null + public final String service; + public final String name; + + private HashMap extraFields; + private byte[] signatureBlock; + private byte[] textFormat; + + protected RhizomeManifest( BundleId id, + long version, + long filesize, + FileHash filehash, + SubscriberId sender, + SubscriberId recipient, + BundleKey BK, + Integer crypt, + Long tail, + Long date, + String service, + String name + ) + { + assert id != null; + if (filesize == 0) + assert filehash == null; + else + assert filehash != null; + this.id = id; + this.version = version; + this.filesize = filesize; + this.filehash = filehash; + this.sender = sender; + this.recipient = recipient; + this.BK = BK; + this.crypt = crypt; + this.tail = tail; + this.date = date; + this.service = service; + this.name = name; + this.extraFields = null; + this.signatureBlock = null; + this.textFormat = null; + } + + /** Return the Rhizome manifest in its text format representation. + * + * @author Andrew Bettison + */ + public byte[] toTextFormat() throws RhizomeManifestSizeException + { + if (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(); + if (os.size() > TEXT_FORMAT_MAX_SIZE) + throw new RhizomeManifestSizeException("manifest text format overflow", os.size(), TEXT_FORMAT_MAX_SIZE); + 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); + return ret; + } + + /** Construct a Rhizome manifest from its text format representation. + * + * @author Andrew Bettison + */ + static public RhizomeManifest fromTextFormat(byte[] bytes) throws RhizomeManifestParseException + { + return fromTextFormat(bytes, 0, bytes.length); + } + + /** Construct a Rhizome manifest from its text format representation. + * + * @author Andrew Bettison + */ + static public RhizomeManifest fromTextFormat(byte[] bytes, int off, int len) throws RhizomeManifestParseException + { + // The signature block follows the first nul character at the start of a line. + byte[] sigblock = null; + int proplen = len; + for (int i = 0; i < len; ++i) { + if (bytes[off + i] == 0 && (i == 0 || bytes[off + i - 1] == '\n')) { + sigblock = new byte[len - i - 1]; + System.arraycopy(bytes, off + i + 1, sigblock, 0, sigblock.length); + proplen = i; + break; + } + } + String text; + try { + text = new String(bytes, off, proplen, "US-ASCII"); + } + catch (UnsupportedEncodingException e) { + throw new RhizomeManifestParseException(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) + throw new RhizomeManifestParseException("missing 'id' field"); + if (version == null) + throw new RhizomeManifestParseException("missing 'version' field"); + if (filesize == null) + throw new RhizomeManifestParseException("missing 'filesize' field"); + if (filesize != 0 && filehash == null) + throw new RhizomeManifestParseException("missing 'filehash' field"); + else if (filesize == 0 && 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; + m.signatureBlock = sigblock; + m.textFormat = new byte[len]; + System.arraycopy(bytes, off, m.textFormat, 0, m.textFormat.length); + return m; + } + + /** Convenience method: construct a Rhizome manifest from all the bytes read from a given + * InputStream. + * + * @author Andrew Bettison + */ + static public RhizomeManifest fromTextFormat(InputStream in) throws IOException, RhizomeManifestParseException + { + byte[] bytes = new byte[TEXT_FORMAT_MAX_SIZE]; + int n = 0; + int offset = 0; + while (offset < bytes.length && (n = in.read(bytes, offset, bytes.length - offset)) != -1) + offset += n; + assert offset <= bytes.length; + if (offset == bytes.length) + n = in.read(); + if (n != -1) + throw new RhizomeManifestParseException("input stream too long"); + return fromTextFormat(bytes, 0, offset); + } + + 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/RhizomeManifestBundle.java b/java/org/servalproject/servaldna/rhizome/RhizomeManifestBundle.java new file mode 100644 index 00000000..452aa73d --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeManifestBundle.java @@ -0,0 +1,55 @@ +/** + * 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 RhizomeManifestBundle { + + public final long insertTime; + public final SubscriberId author; + public final BundleSecret secret; + public final RhizomeManifest manifest; + + protected RhizomeManifestBundle(RhizomeManifest manifest, + long insertTime, + SubscriberId author, + BundleSecret secret) + + { + this.manifest = manifest; + this.insertTime = insertTime; + this.author = author; + this.secret = secret; + } + + public byte[] manifestText() throws ServalDInterfaceException + { + try { + return manifest.toTextFormat(); + } + catch (RhizomeManifestSizeException e) { + throw new ServalDInterfaceException("manifest text overflow", e); + } + } +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeManifestParseException.java b/java/org/servalproject/servaldna/rhizome/RhizomeManifestParseException.java new file mode 100644 index 00000000..7f8b4043 --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeManifestParseException.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; + +/** + * Thrown when a Rhizome manifest cannot be parsed from its text format. + * + * @author Andrew Bettison + */ +public class RhizomeManifestParseException extends Exception +{ + public RhizomeManifestParseException(String message) { + super(message); + } + + public RhizomeManifestParseException(Throwable cause) { + super(cause); + } + + public RhizomeManifestParseException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeManifestSizeException.java b/java/org/servalproject/servaldna/rhizome/RhizomeManifestSizeException.java new file mode 100644 index 00000000..88e69adc --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeManifestSizeException.java @@ -0,0 +1,48 @@ +/** + * 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.io.File; + +/** + * Thrown when the text or binary format of a Rhizome manifest is too long to fit in a limited-size + * byte stream. + * + * @author Andrew Bettison + */ +public class RhizomeManifestSizeException extends Exception +{ + private long mSize; + private long mMaxSize; + + public RhizomeManifestSizeException(String message, long size, long maxSize) + { + super(message + " (" + size + " bytes exceeds " + maxSize + ")"); + mSize = size; + mMaxSize = maxSize; + } + + public RhizomeManifestSizeException(File manifestFile, long maxSize) + { + this(manifestFile.toString(), manifestFile.length(), maxSize); + } + +} diff --git a/java/org/servalproject/test/Rhizome.java b/java/org/servalproject/test/Rhizome.java index c54fabca..5538c13a 100644 --- a/java/org/servalproject/test/Rhizome.java +++ b/java/org/servalproject/test/Rhizome.java @@ -21,39 +21,50 @@ package org.servalproject.test; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.FileOutputStream; import org.servalproject.servaldna.ServalDClient; import org.servalproject.servaldna.ServalDInterfaceException; import org.servalproject.servaldna.ServerControl; -import org.servalproject.servaldna.SubscriberId; -import org.servalproject.servaldna.rhizome.RhizomeBundle; +import org.servalproject.servaldna.BundleId; +import org.servalproject.servaldna.rhizome.RhizomeManifest; +import org.servalproject.servaldna.rhizome.RhizomeListBundle; import org.servalproject.servaldna.rhizome.RhizomeBundleList; +import org.servalproject.servaldna.rhizome.RhizomeManifestBundle; 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; + } + static void rhizome_list() throws ServalDInterfaceException, IOException, InterruptedException { ServalDClient client = new ServerControl().getRestfulClient(); RhizomeBundleList list = null; try { list = client.rhizomeListBundles(); - RhizomeBundle bundle; + RhizomeListBundle bundle; while ((bundle = list.nextBundle()) != null) { System.out.println( - "_id=" + bundle._id + - ", .token=" + bundle._token + - ", service=" + bundle.service + - ", id=" + bundle.id + - ", version=" + bundle.version + - ", date=" + bundle.date + - ", .inserttime=" + bundle._inserttime + - ", .author=" + bundle._author + - ", .fromhere=" + bundle._fromhere + - ", filesize=" + bundle.filesize + - ", filehash=" + bundle.filehash + - ", sender=" + bundle.sender + - ", recipient=" + bundle.recipient + - ", name=" + bundle.name - ); + "_rowId=" + bundle.rowId + + ", _token=" + bundle.token + + ", _insertTime=" + bundle.insertTime + + ", _author=" + bundle.author + + ", _fromHere=" + bundle.fromHere + + ", " + manifestFields(bundle.manifest, ", ") + ); } } finally { @@ -63,6 +74,22 @@ public class Rhizome { System.exit(0); } + static void rhizome_manifest(BundleId bid, String dstpath) throws ServalDInterfaceException, IOException, InterruptedException + { + ServalDClient client = new ServerControl().getRestfulClient(); + RhizomeManifestBundle bundle = client.rhizomeManifest(bid); + System.out.println( + "_insertTime=" + bundle.insertTime + "\n" + + "_author=" + bundle.author + "\n" + + "_secret=" + bundle.secret + "\n" + + manifestFields(bundle.manifest, "\n") + "\n" + ); + FileOutputStream out = new FileOutputStream(dstpath); + out.write(bundle.manifestText()); + out.close(); + System.exit(0); + } + public static void main(String... args) { if (args.length < 1) @@ -71,6 +98,8 @@ public class Rhizome { try { if (methodName.equals("rhizome-list")) rhizome_list(); + else if (methodName.equals("rhizome-manifest")) + rhizome_manifest(new BundleId(args[1]), args[2]); } catch (Exception e) { e.printStackTrace(); System.exit(1); diff --git a/rhizome_restful.c b/rhizome_restful.c index 5a178e36..4c5647b8 100644 --- a/rhizome_restful.c +++ b/rhizome_restful.c @@ -595,13 +595,17 @@ int restful_rhizome_(httpd_request *r, const char *remainder) if ((r->manifest = rhizome_new_manifest()) == NULL) return 500; ret = rhizome_retrieve_manifest(&bid, r->manifest); - if (ret == -1) + if (ret == -1) { + rhizome_manifest_free(r->manifest); + r->manifest = NULL; return 500; + } if (ret == 0) { rhizome_authenticate_author(r->manifest); r->http.render_extra_headers = render_manifest_headers; } else { - assert(r->manifest == NULL); + rhizome_manifest_free(r->manifest); + r->manifest = NULL; assert(r->http.render_extra_headers == NULL); } ret = handler(r, remainder); diff --git a/tests/rhizomejava b/tests/rhizomejava index 54fa7abf..ea0494b5 100755 --- a/tests/rhizomejava +++ b/tests/rhizomejava @@ -117,25 +117,56 @@ test_RhizomeList() { let lnum=NBUNDLES for ((n = 0; n != NBUNDLES; ++n)); do line="$(sed -n -e ${lnum}p "$TFWSTDOUT")" - unset_vars_with_prefix X__ - unpack_vars X__ "$line" + unset_vars_with_prefix XX_ + unpack_vars XX_ "$line" if [ "${ROWID[$n]}" -eq "$ROWID_MAX" ]; then # The first row must contain a non-null token string. - assert [ -n "$X____token" ] + assert [ -n "$XX__token" ] assert [ "$lnum" -eq 1 ] fi - assert [ "$X__name" = "file$n" ] - assert [ "$X__service" = "file" ] - assert [ "$X__id" = "${BID[$n]}" ] - assert [ "$X__version" = "${VERSION[$n]}" ] - assert [ "$X__filesize" = "${SIZE[$n]}" ] - assert [ "$X__filehash" = "${HASH[$n]}" ] - assert [ "$X__date" = "${DATE[$n]}" ] - assert [ "$X___id" = "${ROWID[$n]}" ] - assert [ "$X____fromhere" = "1" ] - assert [ "$X____author" = "$SIDA1" ] + assert [ "$XX_id" = "${BID[$n]}" ] + assert [ "$XX_version" = "${VERSION[$n]}" ] + assert [ "$XX_filesize" = "${SIZE[$n]}" ] + assert [ "$XX_filehash" = "${HASH[$n]}" ] + assert [ "$XX_date" = "${DATE[$n]}" ] + assert [ "$XX_service" = "file" ] + assert [ "$XX_name" = "file$n" ] + assert [ "$XX__rowId" = "${ROWID[$n]}" ] + assert [ "$XX__fromHere" = "1" ] + assert [ "$XX__author" = "$SIDA1" ] + assert [ "$XX__insertTime" = "${INSERTTIME[$n]}" ] let --lnum done } +assert_metadata() { + local n=$1 + assertStdoutGrep --matches=1 "^id=${BID[$n]}$CR\$" + assertStdoutGrep --matches=1 "^version=${VERSION[$n]}$CR\$" + assertStdoutGrep --matches=1 "^filesize=${SIZE[$n]}$CR\$" + assertStdoutGrep --matches=1 "^filehash=${HASH[$n]}$CR\$" + assertStdoutGrep --matches=1 "^BK=${BK[$n]}$CR\$" + assertStdoutGrep --matches=1 "^date=${DATE[$n]}$CR\$" + assertStdoutGrep --matches=1 "^name=${NAME[$n]}$CR\$" + assertStdoutGrep --matches=1 "^service=file$CR\$" + assertStdoutGrep --matches=1 "^_insertTime=${INSERTTIME[$n]}$CR\$" + assertStdoutGrep --matches=1 "^_author=${AUTHOR[$n]}$CR\$" + assertStdoutGrep --matches=1 "^_secret=${SECRET[$n]}$CR\$" +} + +doc_RhizomeManifest="Java API fetch Rhizome manifest" +setup_RhizomeManifest() { + setup + rhizome_add_bundles $SIDA1 0 2 +} +test_RhizomeManifest() { + for n in 0 1 2; do + executeJavaOk org.servalproject.test.Rhizome rhizome-manifest ${BID[$n]} bundle$n.rhm + tfw_cat --stdout --stderr + assert_metadata $n + tfw_cat -v file$n.manifest -v bundle$n.rhm + assert diff file$n.manifest bundle$n.rhm + done +} + runTests "$@"