diff --git a/java/org/servalproject/servaldna/ServalDClient.java b/java/org/servalproject/servaldna/ServalDClient.java index 325a5845..c7b35778 100644 --- a/java/org/servalproject/servaldna/ServalDClient.java +++ b/java/org/servalproject/servaldna/ServalDClient.java @@ -27,9 +27,18 @@ import org.servalproject.servaldna.meshms.MeshMSMessageList; import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; +import java.net.HttpURLConnection; +import org.servalproject.codec.Base64; +import org.servalproject.servaldna.SubscriberId; +import org.servalproject.servaldna.ServalDCommand; +import org.servalproject.servaldna.ServalDInterfaceException; +import org.servalproject.servaldna.meshms.MeshMSCommon; +import org.servalproject.servaldna.meshms.MeshMSConversationList; +import org.servalproject.servaldna.meshms.MeshMSMessageList; +import org.servalproject.servaldna.meshms.MeshMSException; +import org.servalproject.servaldna.meshms.MeshMSStatus; public class ServalDClient implements ServalDHttpConnectionFactory { @@ -63,6 +72,11 @@ public class ServalDClient implements ServalDHttpConnectionFactory return list; } + public MeshMSStatus meshmsSendMessage(SubscriberId sid1, SubscriberId sid2, String text) throws IOException, ServalDInterfaceException, MeshMSException + { + return MeshMSCommon.sendMessage(this, sid1, sid2, text); + } + // interface ServalDHttpConnectionFactory public HttpURLConnection newServalDHttpConnection(String path) throws ServalDInterfaceException, IOException { diff --git a/java/org/servalproject/servaldna/ServalDFailureException.java b/java/org/servalproject/servaldna/ServalDFailureException.java index 90296a86..098d8ff1 100644 --- a/java/org/servalproject/servaldna/ServalDFailureException.java +++ b/java/org/servalproject/servaldna/ServalDFailureException.java @@ -21,8 +21,9 @@ package org.servalproject.servaldna; /** - * Thrown when a request to a servald JNI method fails. This typically means that the returned - * status is non-zero, or some other result was returned that indicated the operation failed. + * Thrown when servald returns an error result from an operation, whether JNI or RESTful HTTP + * request, or MDP command. This typically means that the returned status is non-zero, or some + * other result was returned that indicated the operation failed. * * @author Andrew Bettison */ diff --git a/java/org/servalproject/servaldna/ServerControl.java b/java/org/servalproject/servaldna/ServerControl.java index 081e5cc0..f17410c1 100644 --- a/java/org/servalproject/servaldna/ServerControl.java +++ b/java/org/servalproject/servaldna/ServerControl.java @@ -93,6 +93,12 @@ public class ServerControl { if (!isRunning()) throw new ServalDInterfaceException("server is not running"); if (client==null) { + /* TODO: replace the following username/password configuration with a better scheme + * (outside the scope of this API) that does not require any invocations of the JNI, and + * allows any application (user) on the local host to request authorisation to use the + * RESTful interface. The authorisation must then be supplied to the restful client + * object before requests can be made. + */ String restfulPassword = ServalDCommand.getConfigItem("rhizome.api.restful.users." + restfulUsername + ".password"); if (restfulPassword == null) { String pwd = new BigInteger(130, new SecureRandom()).toString(32); diff --git a/java/org/servalproject/servaldna/meshms/MeshMSCommon.java b/java/org/servalproject/servaldna/meshms/MeshMSCommon.java index dbe89667..5a7bbad1 100644 --- a/java/org/servalproject/servaldna/meshms/MeshMSCommon.java +++ b/java/org/servalproject/servaldna/meshms/MeshMSCommon.java @@ -22,51 +22,101 @@ package org.servalproject.servaldna.meshms; import java.io.IOException; import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintStream; +import java.net.URL; import java.net.HttpURLConnection; +import org.servalproject.servaldna.SubscriberId; +import org.servalproject.servaldna.ServalDHttpConnectionFactory; import org.servalproject.servaldna.ServalDInterfaceException; +import org.servalproject.servaldna.ServalDFailureException; import org.servalproject.json.JSONTokeniser; import org.servalproject.json.JSONInputException; -class MeshMSCommon +public class MeshMSCommon { - protected static JSONTokeniser connectMeshMSRestful(HttpURLConnection conn) throws IOException, ServalDInterfaceException, MeshMSException + protected static JSONTokeniser receiveRestfulResponse(HttpURLConnection conn, int expected_response_code) throws IOException, ServalDInterfaceException, MeshMSException { - conn.connect(); 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")); - try { - json.consume(JSONTokeniser.Token.START_OBJECT); - json.consume("http_status_code"); - json.consume(JSONTokeniser.Token.COLON); - json.consume(Integer.class); - json.consume(JSONTokeniser.Token.COMMA); - json.consume("http_status_message"); - json.consume(JSONTokeniser.Token.COLON); - String message = json.consume(String.class); - json.consume(JSONTokeniser.Token.COMMA); - json.consume("meshms_status_code"); - json.consume(JSONTokeniser.Token.COLON); - int meshms_status = json.consume(Integer.class); - json.consume(JSONTokeniser.Token.END_OBJECT); - json.consume(JSONTokeniser.Token.EOF); - switch (meshms_status) { - case 2: - throw new MeshMSUnknownIdentityException(conn.getURL()); - case 3: - throw new MeshMSProtocolFaultException(conn.getURL()); - } - throw new ServalDInterfaceException("unexpected MeshMS status = " + meshms_status + ", \"" + message + "\""); - } - catch (JSONInputException e) { - throw new ServalDInterfaceException("malformed response body for HTTP status code " + conn.getResponseCode(), e); - } + Status status = decodeRestfulStatus(json); + throwRestfulResponseExceptions(status, conn.getURL()); + throw new ServalDInterfaceException("unexpected MeshMS status = " + status.meshms_status + ", \"" + status.message + "\""); } - if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) + if (conn.getResponseCode() != expected_response_code) throw new ServalDInterfaceException("unexpected HTTP response code: " + conn.getResponseCode()); JSONTokeniser json = new JSONTokeniser(new InputStreamReader(conn.getInputStream(), "US-ASCII")); return json; } + private static class Status { + public MeshMSStatus meshms_status; + public String message; + } + + protected static Status decodeRestfulStatus(JSONTokeniser json) throws IOException, ServalDInterfaceException + { + try { + Status status = new Status(); + json.consume(JSONTokeniser.Token.START_OBJECT); + json.consume("http_status_code"); + json.consume(JSONTokeniser.Token.COLON); + json.consume(Integer.class); + json.consume(JSONTokeniser.Token.COMMA); + status.message = json.consume("http_status_message"); + json.consume(JSONTokeniser.Token.COLON); + String message = json.consume(String.class); + json.consume(JSONTokeniser.Token.COMMA); + json.consume("meshms_status_code"); + json.consume(JSONTokeniser.Token.COLON); + status.meshms_status = MeshMSStatus.fromCode(json.consume(Integer.class)); + json.consume(JSONTokeniser.Token.END_OBJECT); + json.consume(JSONTokeniser.Token.EOF); + return status; + } + catch (JSONInputException e) { + throw new ServalDInterfaceException("malformed JSON status response", e); + } + } + + protected static void throwRestfulResponseExceptions(Status status, URL url) throws MeshMSException, ServalDFailureException + { + switch (status.meshms_status) { + case OK: + case UPDATED: + break; + case SID_LOCKED: + throw new MeshMSUnknownIdentityException(url); + case PROTOCOL_FAULT: + throw new MeshMSProtocolFaultException(url); + case ERROR: + throw new ServalDFailureException("received meshms_status=ERROR(-1) from " + url); + } + } + + public static MeshMSStatus sendMessage(ServalDHttpConnectionFactory connector, SubscriberId sid1, SubscriberId sid2, String text) throws IOException, ServalDInterfaceException, MeshMSException + { + HttpURLConnection conn = connector.newServalDHttpConnection("/restful/meshms/" + sid1.toHex() + "/" + sid2.toHex() + "/sendmessage"); + 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"); + wr.print("--" + boundary + "\r\n"); + wr.print("Content-Disposition: form-data; name=\"message\"\r\n"); + wr.print("Content-Type: text/plain; charset=utf-8\r\n"); + wr.print("\r\n"); + wr.print(text); + wr.print("\r\n--" + boundary + "--\r\n"); + wr.close(); + JSONTokeniser json = MeshMSCommon.receiveRestfulResponse(conn, HttpURLConnection.HTTP_CREATED); + Status status = decodeRestfulStatus(json); + throwRestfulResponseExceptions(status, conn.getURL()); + return status.meshms_status; + } + } diff --git a/java/org/servalproject/servaldna/meshms/MeshMSConversationList.java b/java/org/servalproject/servaldna/meshms/MeshMSConversationList.java index bbdf1207..836669ef 100644 --- a/java/org/servalproject/servaldna/meshms/MeshMSConversationList.java +++ b/java/org/servalproject/servaldna/meshms/MeshMSConversationList.java @@ -69,7 +69,8 @@ public class MeshMSConversationList { columnIndex_read_offset = -1; rowCount = 0; httpConnection = httpConnector.newServalDHttpConnection("/restful/meshms/" + sid.toHex() + "/conversationlist.json"); - json = MeshMSCommon.connectMeshMSRestful(httpConnection); + httpConnection.connect(); + json = MeshMSCommon.receiveRestfulResponse(httpConnection, HttpURLConnection.HTTP_OK); json.consume(JSONTokeniser.Token.START_OBJECT); json.consume("header"); json.consume(JSONTokeniser.Token.COLON); diff --git a/java/org/servalproject/servaldna/meshms/MeshMSMessageList.java b/java/org/servalproject/servaldna/meshms/MeshMSMessageList.java index 6557c553..cdc2d28f 100644 --- a/java/org/servalproject/servaldna/meshms/MeshMSMessageList.java +++ b/java/org/servalproject/servaldna/meshms/MeshMSMessageList.java @@ -79,7 +79,8 @@ public class MeshMSMessageList { columnIndex_ack_offset = -1; rowCount = 0; httpConnection = httpConnector.newServalDHttpConnection("/restful/meshms/" + my_sid.toHex() + "/" + their_sid.toHex() + "/messagelist.json"); - json = MeshMSCommon.connectMeshMSRestful(httpConnection); + httpConnection.connect(); + json = MeshMSCommon.receiveRestfulResponse(httpConnection, HttpURLConnection.HTTP_OK); json.consume(JSONTokeniser.Token.START_OBJECT); json.consume("read_offset"); json.consume(JSONTokeniser.Token.COLON); diff --git a/java/org/servalproject/servaldna/meshms/MeshMSStatus.java b/java/org/servalproject/servaldna/meshms/MeshMSStatus.java new file mode 100644 index 00000000..cf3069f7 --- /dev/null +++ b/java/org/servalproject/servaldna/meshms/MeshMSStatus.java @@ -0,0 +1,63 @@ +/** + * 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.meshms; + +import org.servalproject.servaldna.ServalDInterfaceException; + +/* This enum is a direct isomorphism from the C "enum meshms_result" defined in meshms.h. + */ +public enum MeshMSStatus { + ERROR(-1), // unexpected error (underlying failure) + OK(0), // operation succeeded, no bundle changed + UPDATED(1), // operation succeeded, bundle updated + SID_LOCKED(2), // cannot decode or send messages for that SID + PROTOCOL_FAULT(3), // missing or faulty ply bundle + ; + + final public int code; + + private MeshMSStatus(int code) { + this.code = code; + } + + public static MeshMSStatus fromCode(int code) throws InvalidException + { + MeshMSStatus status = null; + switch (code) { + case -1: status = ERROR; break; + case 0: status = OK; break; + case 1: status = UPDATED; break; + case 2: status = SID_LOCKED; break; + case 3: status = PROTOCOL_FAULT; break; + default: throw new InvalidException(code); + } + assert status.code == code; + return status; + } + + public static class InvalidException extends ServalDInterfaceException + { + public InvalidException(int code) { + super("invalid MeshMS status code = " + code); + } + } + +} diff --git a/java/org/servalproject/test/Meshms.java b/java/org/servalproject/test/Meshms.java index 073001d8..7e531378 100644 --- a/java/org/servalproject/test/Meshms.java +++ b/java/org/servalproject/test/Meshms.java @@ -20,6 +20,7 @@ package org.servalproject.test; +import java.io.IOException; import org.servalproject.servaldna.ServalDClient; import org.servalproject.servaldna.ServalDInterfaceException; import org.servalproject.servaldna.ServerControl; @@ -29,8 +30,7 @@ import org.servalproject.servaldna.meshms.MeshMSConversationList; import org.servalproject.servaldna.meshms.MeshMSException; import org.servalproject.servaldna.meshms.MeshMSMessage; import org.servalproject.servaldna.meshms.MeshMSMessageList; - -import java.io.IOException; +import org.servalproject.servaldna.meshms.MeshMSStatus; public class Meshms { @@ -94,6 +94,19 @@ public class Meshms { System.exit(0); } + static void meshms_send_message(SubscriberId sid1, SubscriberId sid2, String text) throws ServalDInterfaceException, IOException, InterruptedException + { + ServalDClient client = new ServerControl().getRestfulClient(); + try { + MeshMSStatus status = client.meshmsSendMessage(sid1, sid2, text); + System.out.println("" + status); + } + catch (MeshMSException e) { + System.out.println(e.toString()); + } + System.exit(0); + } + public static void main(String... args) { if (args.length < 1) @@ -104,6 +117,8 @@ public class Meshms { meshms_list_conversations(new SubscriberId(args[1])); else if (methodName.equals("meshms-list-messages")) meshms_list_messages(new SubscriberId(args[1]), new SubscriberId(args[2])); + else if (methodName.equals("meshms-send-message")) + meshms_send_message(new SubscriberId(args[1]), new SubscriberId(args[2]), args[3]); } catch (Exception e) { e.printStackTrace(); System.exit(1); diff --git a/tests/meshmsjava b/tests/meshmsjava index 05cc7e17..b3398ed3 100755 --- a/tests/meshmsjava +++ b/tests/meshmsjava @@ -137,14 +137,30 @@ test_MeshmsListMessages() { } doc_MeshmsListMessagesNoIdentity="Java API list MeshMS messages from unknown identity" -setup_MeshmsListMessagesNoIdentity() { - setup - SIDX=0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF -} test_MeshmsListMessagesNoIdentity() { + SIDX=0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF executeJavaOk org.servalproject.test.Meshms meshms-list-messages $SIDX $SIDA2 assertStdoutGrep 'MeshMSUnknownIdentityException' tfw_cat --stdout --stderr } +doc_MeshmsSend="Java API send MeshMS message" +test_MeshmsSend() { + executeJavaOk org.servalproject.test.Meshms meshms-send-message $SIDA1 $SIDA2 "Hello World" + executeOk_servald meshms list messages $SIDA1 $SIDA2 + assertStdoutGrep --matches=1 ':>:Hello World' + executeJavaOk org.servalproject.test.Meshms meshms-send-message $SIDA2 $SIDA1 "Hello Back!" + executeOk_servald meshms list messages $SIDA1 $SIDA2 + assertStdoutGrep --matches=1 ':>:Hello World$' + assertStdoutGrep --matches=1 ':<:Hello Back!$' +} + +doc_MeshmsSendNoIdentity="Java API send MeshMS message from unknown identity" +test_MeshmsSendNoIdentity() { + SIDX=0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF + executeJavaOk org.servalproject.test.Meshms meshms-send-message $SIDX $SIDA2 "Hello World" + assertStdoutGrep 'MeshMSUnknownIdentityException' + tfw_cat --stdout --stderr +} + runTests "$@"