diff --git a/java-api/src/org/servalproject/servaldna/ServalDClient.java b/java-api/src/org/servalproject/servaldna/ServalDClient.java index 614c864a..638ba577 100644 --- a/java-api/src/org/servalproject/servaldna/ServalDClient.java +++ b/java-api/src/org/servalproject/servaldna/ServalDClient.java @@ -24,6 +24,7 @@ import org.servalproject.codec.Base64; import org.servalproject.servaldna.keyring.KeyringCommon; import org.servalproject.servaldna.keyring.KeyringIdentity; import org.servalproject.servaldna.keyring.KeyringIdentityList; +import org.servalproject.servaldna.route.RouteIdentityList; import org.servalproject.servaldna.meshmb.MeshMBActivityList; import org.servalproject.servaldna.meshmb.MeshMBCommon; import org.servalproject.servaldna.meshmb.MeshMBSubscriptionList; @@ -114,6 +115,12 @@ public class ServalDClient implements ServalDHttpConnectionFactory { return KeyringCommon.removeIdentity(this, sid, pin); } + public RouteIdentityList routeListIdentities() throws ServalDInterfaceException, IOException { + RouteIdentityList list = new RouteIdentityList(this); + list.connect(); + return list; + } + public RhizomeBundleList rhizomeListBundles() throws ServalDInterfaceException, IOException { RhizomeBundleList list = new RhizomeBundleList(this); diff --git a/java-api/src/org/servalproject/servaldna/route/RouteCommon.java b/java-api/src/org/servalproject/servaldna/route/RouteCommon.java new file mode 100644 index 00000000..e69f2ff4 --- /dev/null +++ b/java-api/src/org/servalproject/servaldna/route/RouteCommon.java @@ -0,0 +1,146 @@ +/** + * Copyright (C) 2016-2018 Flinders University + * Copyright (C) 2015 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.route; + +import org.servalproject.json.JSONInputException; +import org.servalproject.json.JSONTokeniser; +import org.servalproject.servaldna.ContentType; +import org.servalproject.servaldna.ServalDInterfaceException; +import org.servalproject.servaldna.ServalDNotImplementedException; +import org.servalproject.servaldna.SubscriberId; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; + +public class RouteCommon +{ + + public static class Status { + ContentType contentType; + InputStream input_stream; + JSONTokeniser json; + public int http_status_code; + public String http_status_message; + } + + protected static Status 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 Status receiveResponse(HttpURLConnection conn, int[] expected_response_codes) throws IOException, ServalDInterfaceException + { + Status status = new Status(); + status.http_status_code = conn.getResponseCode(); + status.http_status_message = conn.getResponseMessage(); + try { + status.contentType = new ContentType(conn.getContentType()); + } catch (ContentType.ContentTypeException e) { + throw new ServalDInterfaceException("malformed HTTP Content-Type: " + conn.getContentType(),e); + } + + for (int code: expected_response_codes) { + if (status.http_status_code == code) { + status.input_stream = conn.getInputStream(); + return status; + } + } + if (!ContentType.applicationJson.matches(status.contentType)) + throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + status.contentType); + if (status.http_status_code >= 300) { + status.json = new JSONTokeniser(conn.getErrorStream()); + decodeRestfulStatus(status); + } + if (status.http_status_code == HttpURLConnection.HTTP_FORBIDDEN) + return status; + if (status.http_status_code == HttpURLConnection.HTTP_NOT_IMPLEMENTED) + throw new ServalDNotImplementedException(status.http_status_message); + throw new ServalDInterfaceException("unexpected HTTP response: " + status.http_status_code + " " + status.http_status_message); + } + + protected static ServalDInterfaceException unexpectedResponse(HttpURLConnection conn, Status status) + { + return new ServalDInterfaceException( + "unexpected Route failure, " + quoteString(status.http_status_message) + + " from " + conn.getURL() + ); + } + + protected static Status receiveRestfulResponse(HttpURLConnection conn, int expected_response_code) throws IOException, ServalDInterfaceException + { + int[] expected_response_codes = { expected_response_code }; + return receiveRestfulResponse(conn, expected_response_codes); + } + + protected static Status receiveRestfulResponse(HttpURLConnection conn, int[] expected_response_codes) throws IOException, ServalDInterfaceException + { + Status status = receiveResponse(conn, expected_response_codes); + status.json = new JSONTokeniser(status.input_stream); + return status; + } + + protected static void decodeRestfulStatus(Status status) throws IOException, ServalDInterfaceException + { + JSONTokeniser json = status.json; + try { + json.consume(JSONTokeniser.Token.START_OBJECT); + json.consume("http_status_code"); + json.consume(JSONTokeniser.Token.COLON); + int hs = json.consume(Integer.class); + json.consume(JSONTokeniser.Token.COMMA); + if (status.http_status_code == 0) + status.http_status_code = json.consume(Integer.class); + else if (hs != status.http_status_code) + throw new ServalDInterfaceException("JSON/header conflict" + + ", http_status_code=" + hs + + " but HTTP response code is " + status.http_status_code); + json.consume("http_status_message"); + json.consume(JSONTokeniser.Token.COLON); + status.http_status_message = json.consume(String.class); + json.consume(JSONTokeniser.Token.END_OBJECT); + json.consume(JSONTokeniser.Token.EOF); + } + catch (JSONInputException e) { + throw new ServalDInterfaceException("malformed JSON status response", e); + } + } + + private static String quoteString(String unquoted) + { + if (unquoted == null) + return "null"; + 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(); + } + +} + diff --git a/java-api/src/org/servalproject/servaldna/route/RouteIdentity.java b/java-api/src/org/servalproject/servaldna/route/RouteIdentity.java new file mode 100644 index 00000000..cf27daae --- /dev/null +++ b/java-api/src/org/servalproject/servaldna/route/RouteIdentity.java @@ -0,0 +1,62 @@ +/** + * Copyright (C) 2016-2018 Flinders University + * Copyright (C) 2012-2015 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.route; + +import org.servalproject.servaldna.SubscriberId; + +public class RouteIdentity { + + public final int rowNumber; + public final SubscriberId sid; + public final String did; + public final String name; + public final boolean isSelf; + public final int hopCount; + public final boolean reachableBroadcast; + public final boolean reachableUnicast; + public final boolean reachableIndirect; + + protected RouteIdentity(int rowNumber, + SubscriberId sid, + String did, + String name, + boolean isSelf, + int hopCount, + boolean reachableBroadcast, + boolean reachableUnicast, + boolean reachableIndirect) + { + assert(rowNumber >= 0); + assert(sid != null); + assert(hopCount >= 0); + this.rowNumber = rowNumber; + this.sid = sid; + this.did = did; + this.name = name; + this.isSelf = isSelf; + this.hopCount = hopCount; + this.reachableBroadcast = reachableBroadcast; + this.reachableUnicast = reachableUnicast; + this.reachableIndirect = reachableIndirect; + } + +} diff --git a/java-api/src/org/servalproject/servaldna/route/RouteIdentityList.java b/java-api/src/org/servalproject/servaldna/route/RouteIdentityList.java new file mode 100644 index 00000000..e59804d2 --- /dev/null +++ b/java-api/src/org/servalproject/servaldna/route/RouteIdentityList.java @@ -0,0 +1,143 @@ +/** + * Copyright (C) 2016-2018 Flinders University + * Copyright (C) 2014-2015 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.route; + +import org.servalproject.json.JSONInputException; +import org.servalproject.json.JSONTableScanner; +import org.servalproject.json.JSONTokeniser; +import org.servalproject.servaldna.ServalDHttpConnectionFactory; +import org.servalproject.servaldna.ServalDInterfaceException; +import org.servalproject.servaldna.SubscriberId; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.List; +import java.util.Vector; +import java.util.Map; + +public class RouteIdentityList { + + private ServalDHttpConnectionFactory httpConnector; + private HttpURLConnection httpConnection; + private JSONTokeniser json; + private JSONTableScanner table; + int rowCount; + + public RouteIdentityList(ServalDHttpConnectionFactory connector) + { + this.httpConnector = connector; + this.table = new JSONTableScanner() + .addColumn("sid", SubscriberId.class) + .addColumn("did", String.class, JSONTokeniser.Narrow.ALLOW_NULL) + .addColumn("name", String.class, JSONTokeniser.Narrow.ALLOW_NULL) + .addColumn("is_self", Boolean.class) + .addColumn("hop_count", Integer.class) + .addColumn("reachable_broadcast", Boolean.class) + .addColumn("reachable_unicast", Boolean.class) + .addColumn("reachable_indirect", Boolean.class) + ; + } + + public boolean isConnected() + { + return this.json != null; + } + + public void connect() throws IOException, ServalDInterfaceException + { + try { + rowCount = 0; + httpConnection = httpConnector.newServalDHttpConnection("GET", "/restful/route/all.json"); + httpConnection.connect(); + RouteCommon.Status status = RouteCommon.receiveRestfulResponse(httpConnection, HttpURLConnection.HTTP_OK); + json = status.json; + json.consume(JSONTokeniser.Token.START_OBJECT); + json.consume("header"); + json.consume(JSONTokeniser.Token.COLON); + table.consumeHeaderArray(json); + json.consume(JSONTokeniser.Token.COMMA); + json.consume("rows"); + json.consume(JSONTokeniser.Token.COLON); + json.consume(JSONTokeniser.Token.START_ARRAY); + } + catch (JSONInputException e) { + throw new ServalDInterfaceException(e); + } + } + + public static List getTestIdentities() { + try { + List ret = new ArrayList(); + byte[] sid = new byte[SubscriberId.BINARY_SIZE]; + + for (int i = 0; i < 10; i++) { + sid[0]=(byte)i; + ret.add(new RouteIdentity(i, new SubscriberId(sid), "555000" + i, "Agent " + i, i < 5, i, i >= 5, i >= 6, i >= 7)); + } + return ret; + }catch (Exception e){ + throw new IllegalStateException(e); + } + } + + public RouteIdentity nextIdentity() throws ServalDInterfaceException, IOException + { + try { + Object tok = json.nextToken(); + if (tok == JSONTokeniser.Token.END_ARRAY) { + json.consume(JSONTokeniser.Token.END_OBJECT); + json.consume(JSONTokeniser.Token.EOF); + return null; + } + if (rowCount != 0) + JSONTokeniser.match(tok, JSONTokeniser.Token.COMMA); + else + json.pushToken(tok); + Map row = table.consumeRowArray(json); + return new RouteIdentity( + rowCount++, + (SubscriberId)row.get("sid"), + (String)row.get("did"), + (String)row.get("name"), + (Boolean)row.get("is_self"), + (Integer)row.get("hop_count"), + (Boolean)row.get("reachable_broadcast"), + (Boolean)row.get("reachable_unicast"), + (Boolean)row.get("reachable_indirect") + ); + } + catch (JSONInputException e) { + throw new ServalDInterfaceException(e); + } + } + + public void close() throws IOException + { + httpConnection = null; + if (json != null) { + json.close(); + json = null; + } + } + +} diff --git a/java-api/test/org/servalproject/test/Keyring.java b/java-api/test/org/servalproject/test/Keyring.java index 69d80462..8d6fcb59 100644 --- a/java-api/test/org/servalproject/test/Keyring.java +++ b/java-api/test/org/servalproject/test/Keyring.java @@ -1,5 +1,6 @@ /** * Copyright (C) 2015 Serval Project Inc. + * Copyright (C) 2016 Flinders University * * This file is part of Serval Software (http://www.servalproject.org) * diff --git a/java-api/test/org/servalproject/test/Route.java b/java-api/test/org/servalproject/test/Route.java new file mode 100644 index 00000000..16d16dea --- /dev/null +++ b/java-api/test/org/servalproject/test/Route.java @@ -0,0 +1,74 @@ +/** + * Copyright (C) 2015 Serval Project Inc. + * Copyright (C) 2018 Flinders University + * + * 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.test; + +import java.io.IOException; +import org.servalproject.servaldna.ServalDClient; +import org.servalproject.servaldna.ServalDInterfaceException; +import org.servalproject.servaldna.ServerControl; +import org.servalproject.servaldna.route.RouteIdentityList; +import org.servalproject.servaldna.route.RouteIdentity; + +public class Route { + + static void route_list() throws ServalDInterfaceException, IOException, InterruptedException + { + ServalDClient client = new ServerControl().getRestfulClient(); + RouteIdentityList list = null; + try { + list = client.routeListIdentities(); + RouteIdentity id; + while ((id = list.nextIdentity()) != null) { + System.out.println("sid=" + id.sid + + ", did=" + id.did + + ", name=" + id.name + + ", isSelf=" + id.isSelf + + ", hopCount=" + id.hopCount + + ", reachableBroadcast=" + id.reachableBroadcast + + ", reachableUnicast=" + id.reachableUnicast + + ", reachableIndirect=" + id.reachableIndirect + ); + } + } + finally { + if (list != null) + list.close(); + } + System.exit(0); + } + + public static void main(String... args) + { + if (args.length < 1) + return; + String methodName = args[0]; + try { + if (methodName.equals("list-all")) + route_list(); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + System.err.println("No such command: " + methodName); + System.exit(1); + } +} diff --git a/route_restful.c b/route_restful.c index 0ae43801..63fdf834 100644 --- a/route_restful.c +++ b/route_restful.c @@ -146,13 +146,13 @@ static int restful_route_list_json_content_chunk(struct http_request *hr, strbuf strbuf_json_integer(b, subscriber->hop_count); // first_hop strbuf_puts(b, ","); - if (subscriber->hop_count > 1) + if (subscriber->next_hop) strbuf_json_string(b, alloca_tohex_sid_t(subscriber->next_hop->sid)); else strbuf_json_null(b); // penultimate_hop strbuf_puts(b, ","); - if (subscriber->hop_count > 2) + if (subscriber->prior_hop) strbuf_json_string(b, alloca_tohex_sid_t(subscriber->prior_hop->sid)); else strbuf_json_null(b); diff --git a/tests/alljava b/tests/alljava index c7fb0938..921595b0 100755 --- a/tests/alljava +++ b/tests/alljava @@ -3,7 +3,7 @@ # Aggregation of all Java tests. # # Copyright 2012-2014 Serval Project Inc. -# Copyright 2017 Flinders University +# Copyright 2017-2018 Flinders University # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -24,6 +24,7 @@ source "${0%/*}/../testdefs.sh" includeTests jni includeTests keyringjava +includeTests routejava includeTests rhizomejava includeTests meshmsjava diff --git a/tests/routejava b/tests/routejava new file mode 100755 index 00000000..195c110b --- /dev/null +++ b/tests/routejava @@ -0,0 +1,88 @@ +#!/bin/bash + +# Tests for Serval DNA Route Java API +# +# Copyright 2018 Flinders Univerity +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program 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 program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +source "${0%/*}/../testframework.sh" +source "${0%/*}/../testdefs.sh" +source "${0%/*}/../testdefs_routing.sh" +source "${0%/*}/../testdefs_rest.sh" +source "${0%/*}/../testdefs_java.sh" + +setup() { + setup_rest_utilities #XXX + setup_servald + setup_servald_so + assert_java_classes_exist + assert_no_servald_processes +} + +teardown() { + stop_all_servald_servers + kill_all_servald_processes + assert_no_servald_processes + report_all_servald_servers +} + +configure_servald_server() { + executeOk_servald config \ + set debug.mdprequests yes \ + set debug.linkstate yes \ + set debug.subscriber yes \ + set debug.verbose yes \ + set debug.overlayrouting yes \ + set debug.overlayinterfaces yes \ + set log.console.level debug \ + set log.console.show_pid on \ + set log.console.show_time on \ + set rhizome.enable no +} + +doc_RouteListAll="Java API list entire routing table" +setup_RouteListAll() { + setup + setup_rest_config +A #XXX + DIDA1=565656 + NAMEA1="Neddy Seagoon" + DIDA2=3020304 + NAMEA2="Spike Milligan" + foreach_instance +A +B create_identities 2 + foreach_instance +A +B add_servald_interface 1 + foreach_instance +A +B start_servald_server + wait_until_rest_server_ready +A + get_servald_primary_sid +B PRIMARY_SIDB + wait_until --timeout=20 path_exists +A +B + wait_until --timeout=10 path_exists +B +A + set_instance +A + rest_request GET "/restful/route/all.json" #XXX +} +test_RouteListAll() { + executeJavaOk org.servalproject.test.Route list-all + tfw_cat --stdout --stderr + assertStdoutLineCount == 4 + assertStdoutGrep --matches=1 "sid=$SIDA1, did=$DIDA1, name=$NAMEA1, isSelf=true, hopCount=0" + assertStdoutGrep --matches=1 "sid=$SIDA2, did=$DIDA2, name=$NAMEA2, isSelf=true, hopCount=0" + assertStdoutGrep --matches=1 "sid=$PRIMARY_SIDB,.*isSelf=false, hopCount=1,.*reachableUnicast=true,.*reachableIndirect=false" + for SID in "${SIDB[@]}"; do + if [ "$SID" != "$PRIMARY_SIDB" ]; then + assertStdoutGrep --matches=1 "sid=$SID,.*isSelf=false, hopCount=2,.*reachableUnicast=false,.*reachableIndirect=true" + fi + done +} + +runTests "$@" diff --git a/tests/routerestful b/tests/routerestful index 871edf01..38bb282c 100755 --- a/tests/routerestful +++ b/tests/routerestful @@ -87,6 +87,10 @@ test_AuthBasicWrong() { doc_RouteListAll="REST API list entire routing table" setup_RouteListAll() { setup + DIDA1=565656 + NAMEA1="Neddy Seagoon" + DIDA2=3020304 + NAMEA2="Spike Milligan" foreach_instance +A +B create_identities 2 foreach_instance +A +B add_servald_interface 1 foreach_instance +A +B start_servald_server @@ -100,20 +104,24 @@ test_RouteListAll() { rest_request GET "/restful/route/all.json" transform_list_json response.json routes.json assert [ "$(jq 'length' routes.json)" = 4 ] - assertJq routes.json 'contains([{"sid": "'$SIDA1'", + assertJq routes.json 'contains([{"sid": "'"$SIDA1"'", + "did": "'"$DIDA1"'", + "name": "'"$NAMEA1"'", "is_self": true, "hop_count": 0}])' - assertJq routes.json 'contains([{"sid": "'$SIDA2'", + assertJq routes.json 'contains([{"sid": "'"$SIDA2"'", + "did": "'"$DIDA2"'", + "name": "'"$NAMEA2"'", "is_self": true, "hop_count": 0}])' - assertJq routes.json 'contains([{"sid": "'$PRIMARY_SIDB'", + assertJq routes.json 'contains([{"sid": "'"$PRIMARY_SIDB"'", "is_self": false, "reachable_unicast": true, "reachable_indirect": false, "hop_count": 1}])' for SID in "${SIDB[@]}"; do if [ "$SID" != "$PRIMARY_SIDB" ]; then - assertJq routes.json 'contains([{"sid": "'$SID'", + assertJq routes.json 'contains([{"sid": "'"$SID"'", "is_self": false, "reachable_unicast": false, "reachable_indirect": true,