From 8345d896a69d11d3534d5a0109cac1e30c76373e Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Thu, 22 Mar 2018 10:13:08 +1030 Subject: [PATCH] Add Route REST API (fixes #96) Consists of a single, one-shot request, GET /restful/route/all.json, for the time being. A "newsince" request can be added later. Add a Markdown tech doc that specifies the new Route REST API and update the MDP tech doc a little. Add the 'routerestful' test script, which uses some test utility fuctions that have been factored out of the 'routing' test script into the new testdefs_routing.sh. Add the new 'allrestful' test script. --- doc/Mesh-Datagram-Protocol.md | 27 ++--- doc/README.md | 1 + doc/REST-API-Route.md | 208 ++++++++++++++++++++++++++++++++++ httpd.h | 11 +- keyring_restful.c | 3 - overlay_address.c | 27 +++++ overlay_address.h | 11 ++ route_restful.c | 179 +++++++++++++++++++++++++++++ servald_features.c | 1 + sourcefiles.mk | 1 + testdefs.sh | 4 +- testdefs_routing.sh | 95 ++++++++++++++++ tests/all | 9 +- tests/allrestful | 30 +++++ tests/routerestful | 125 ++++++++++++++++++++ tests/routing | 78 +------------ 16 files changed, 711 insertions(+), 99 deletions(-) create mode 100644 doc/REST-API-Route.md create mode 100644 route_restful.c create mode 100644 testdefs_routing.sh create mode 100755 tests/allrestful create mode 100755 tests/routerestful diff --git a/doc/Mesh-Datagram-Protocol.md b/doc/Mesh-Datagram-Protocol.md index 65783642..90fc66f7 100644 --- a/doc/Mesh-Datagram-Protocol.md +++ b/doc/Mesh-Datagram-Protocol.md @@ -8,11 +8,11 @@ make it particularly suitable for use in Ad Hoc wireless networks, which can suffer high levels of packet loss due to weak signal, interference and congestion. -MDP carries [messages](#mdp-message) from [sender](#sender) to -[recipient](#recipient) [node](#node)s, or [broadcasts](#broadcast) to all -nodes. MDP guarantees that message contents will be correct if delivered, but -does not guarantee one delivery (messages may be lost or delivered more than -once), arrival time, or message order. +MDP carries [messages](#mdp-message) from [sender](#sender) to [recipient](#recipient) +[node](#node)s, or [broadcasts](#broadcast) to all nodes. MDP guarantees that +message contents will be correct if delivered, but does not guarantee exactly +one delivery (messages may be lost or delivered more than once), arrival time, +or message order. MDP can be carried over any wireless or wired data link, whether a shared medium (eg, [CSMA/CA][] used in [Wi-Fi][]) or a dedicated medium (eg, [AX.25 @@ -78,7 +78,7 @@ called a *subscriber*), then strictly speaking, the Serval mesh network could be said to carry messages between *users* not between *devices*. There is nothing to prevent a [keyring][] entry from being copied from one device to another, thus it is possible for two or more devices to have the same MDP -Address. At present, Serval routing does not handle this case, so it could +Address. At present, Serval [routing][] does not handle this case, so it could cause unwanted effects such as route flapping or dropped messages. In practice, the “duplicate MDP Address” problem is rare for the time being, @@ -261,15 +261,9 @@ MDP transmits a [MDP message](#mdp-message) over a [link](#link) by encapsulating it into an **overlay packet** (also called **MDP packet** or **overlay frame**). The MDP overlay packet is a [layer 2][] concept; it is only concerned with transporting MDP messages across a single link to a -neighbouring *peer* [node](#node). Once an overlay packet arrives, the -receiver unpacks all of its MDP messages, consumes those for which it (or one -of its zero-hop identities) is the [recipient](#recipient) and independently -routes each of the remaining messages to its next appropriate peer. - -Every overlay packet contains the [MDP address](#mdp-address)es of its -[transmitter](#transmitter) and [receiver](#receiver). - -An overlay packet may contain many MDP messages. The header of each MDP +neighbouring [peer][] node. Once an overlay packet arrives, the receiver +unpacks all of its MDP messages, consumes those for which it (or one of its +[zero-hop][] identities) is the [recipient](#recipient) and independently message in an overlay packet is constructed afresh when it is embedded into the packet, setting its [flag bits](#mdp-message-flags) and re-writing the [address fields](#mdp-address-fields) within the context of the overlay packet, in order @@ -340,6 +334,7 @@ Available under the [Creative Commons Attribution 4.0 International licence][CC [layer 2]: https://en.wikipedia.org/wiki/Data_link_layer [layer 3]: https://en.wikipedia.org/wiki/Network_layer [layer 4]: https://en.wikipedia.org/wiki/Transport_layer +[peer]: ./REST-API-Route.md#peer [UDP]: http://en.wikipedia.org/wiki/User_Datagram_Protocol [MTU]: http://en.wikipedia.org/wiki/Maximum_transmission_unit [SID]: ./REST-API-Keyring.md#serval-id @@ -349,6 +344,8 @@ Available under the [Creative Commons Attribution 4.0 International licence][CC [configured]: ./Servald-Configuration.md [network interfaces]: ./Servald-Configuration.md#network-interfaces [keyring]: ./REST-API-Keyring.md +[routing]: ./REST-API-Route.md +[zero-hop]: ./REST-API-Route.md#zero-hop [encrypted]: http://developer.servalproject.org/dokuwiki/doku.php?id=content:tech:security_framework [signed]: http://developer.servalproject.org/dokuwiki/doku.php?id=content:tech:security_framework [local sockets]: https://en.wikipedia.org/wiki/Unix_domain_socket diff --git a/doc/README.md b/doc/README.md index 2e025c1d..753185ab 100644 --- a/doc/README.md +++ b/doc/README.md @@ -43,6 +43,7 @@ DNA][] component of the [Serval mesh network][]. REST APIs through which applications access the services of the Serval mesh network, including: * [Keyring REST API](./REST-API-Keyring.md) -- identity management + * [Route REST API](./REST-API-Route.md) -- network routing and peers * [Rhizome REST API](./REST-API-Rhizome.md) -- decentralised content distribution * [MeshMS REST API](./REST-API-MeshMS.md) -- secure, one-to-one messaging diff --git a/doc/REST-API-Route.md b/doc/REST-API-Route.md new file mode 100644 index 00000000..336fbd10 --- /dev/null +++ b/doc/REST-API-Route.md @@ -0,0 +1,208 @@ +Route REST API +============== +[Serval Project][], September 2016 + +Introduction +------------ + +Every [Serval DNA][] daemon running on a node in the [Serval mesh network][] +maintains its own dynamic *routing table*, which it uses to choose the [network +interface][] on which to send each outgoing [MDP message][]. The mesh routing +algorithm updates the routing table whenever Serval DNA receives an [overlay +packet][] and whenever system time advances. For more details, see [Mesh +Datagram Protocol][]. + +The [Serval DNA][] daemon gives applications access to its routing table via +the **Route REST API** described in this document. Applications can use this +information to: + + * discover the current [network neighbours](#neighbour) + * discover all currently (and recently) [reachable nodes](#reachable) + * estimate the quality of service to any given [reachable node](#reachable) + +Basic concepts +-------------- + +### Routing table + +Every [node](#node) in the [Serval mesh network][] maintains its own *routing +table*, which identifies a single [path](#path) to every [reachable](#reachable) +[node](#node). The mesh routing algorithm chooses the best path based on +routing information received from other nodes along the path. + +Whenever a [node](#node) receives an [overlay packet][], it knows that, at the +time the packet was received, there existed a direct incoming [link](#link) +from the [transmitting node][transmitter]. It updates its routing table to +mark the transmitter's [primary SID](#primary-sid) as a [neighbour](#neighbour). + +Whenever a [node](#node) receives nothing from a given neighbour for longer +than a [configured][] time interval, it presumes that the [link](#link) is +broken. It updates its routing table to mark the neighbour's [primary +SID](#primary-sid) as no longer a neighbour. + +### Tick + +Every daemon ensures that all of its nearby nodes remain aware of its presence +by sending regular [overlay packet][]s to every [neighbour](#neighbour). Every +[overlay packet][] contains its own [primary SID](#primary-sid) as the sender +address. + +Every [network interface][] has a [configured][] *tick interval*, which is the +maximum time period that may elapse between messages sent to any neighbour on +that interface. If no message has been sent to a given neighbour for a whole +tick interval, then the daemon sends an empty [overlay packet][], called a +*tick packet*, to the neighbour. + +As a result, while an interface is quiescent (no traffic), depending on whether +the [link](#link) to each neighbour is *broadcast* or *unicast*, at every tick +the daemon will send either a single broadcast Wi-Fi packet, or several unicast +Wi-Fi packets, or a mixture. + +### Node + +A *node* in the [Serval mesh network][] is any device with its own link-layer +address (eg, a UDP/IP address or a Wi-Fi MAC address) that is running a [Serval +DNA][] daemon configured to use that network interface. + +### Neighbour + +A [node's](#node) *neighbour* in the [Serval mesh network][] is any node from +which its [Serval DNA][] daemon directly receives [overlay packet][]s through a +[network interface][]. + +Note that a neighbour is not necessarily [reachable](#reachable), because +wireless links are not always symmetrical; even though station A receives from +station B, it does not necessarily mean that B can receive from A, because of +factors like different transmitter power and antenna gain. + +### Link + +A *link* is a one-way connection from a [node](#node) to one of its [neighbour +nodes](#neighbour). Links are represented in the routing table by the [primary +SID](#primary-sid) of their receiving end. + +A link may be either *broadcast* or *unicast*, which is chosen by the receiver +[node](#node) during link negotiation. + +Each link is characterised by a metric that represents the dynamic link quality +(eg, the proportion of recent packets successfully received). Every +[node](#node) dynamically computes the quality of all its incoming links by +counting the gaps in the sequence numbers on received [overlay packet][]s. It +continuously informs each [neighbour node](#neighbour) of the measured quality +of its incoming link by periodically sending a [routing +message](#routing-message) to each neighbour at a [configured][] time interval. + +### Primary SID + +Every [node](#node) identifies itself by its *primary SID*, which is usually +the [SID][] of the first identity that was [unlocked][] since the daemon was +started. + +### Secondary SID + +A [node](#node) may have more than one [SID][], ie more than one [unlocked][] +identity. All its SIDs except the [primary SID](#primary-sid) are called +*secondary*. + +A [node](#node) announces all of its secondary SIDs by representing them in its +[routing messages](#routing-message) as [reachable](#reachable) +[neighbours](#neighbour) on a private network interface (ie, not available to +other nodes) with a 100% link quality. + +### Routing message + +Every [node](#node) in the [Serval mesh network][] informs other nodes of the +presence and quality of all of its incoming and outgoing [links](#link) by +sending *routing messages*. A routing message is a one-hop message that goes +to all [neighbouring](#neighbour) nodes but no further: + + * whenever a node detects the presence (received packet) or absence (timeout) + of an incoming link, or revises the measured quality of an incoming link, + it sends a routing message with the single link's state/quality to the + single [neighbour](#neighbour) at the transmitting end of the link; + + * whenever a node receives a routing message from one of its neighbours, it + incorporates the new link state (up/down) and quality information into its own + routing table, re-evaluates its [paths](#path), and, shortly afterwards, + sends a routing message containing the state/quality information of all its + outgoing and incoming links to all of its [neighbours](#neighbour). + +Routing messages are not forwarded directly, but the information they carry +propagates beyond the node's immediate neighbours because each neighbour, upon +receiving a routing message, updates its own [routing table](#routing-table) +and sends out its own routing messages that arise as a result of the update. + +### Path + +A *path* is a one-way route from a *sender* [node](#node) to a *recipient* +[node](#node), expressed as a sequence of non-repeating [links](#link). The +first link in a path always leads to a [reachable](#reachable) [neighbour +node](#neighbour). + +The routing algorithm constructs paths by choosing links whose existence and +quality has been revealed by a recently-received *routing message* from a +neighbour. + +### Reachable + +A [node](#node) in the [Serval mesh network][] considers another node to be +*reachable* if its own [routing table](#routing-table) contains a [path](#path) +to the second node. + +REST Requests +------------- + +### GET /restful/route/all.json + +Returns a list of all currently known identities, in [JSON table][] format. +The table columns are: + +| heading | type | content | +|:--------------------- |:---------------- |:------------------------------------------------------------------------------ | +| `sid` | string | the [SID][] of the identity, as 64 uppercase hex digits | +| `did` | string or `null` | the [DID][] of the identity if known (eg, for a local [keyring][] identity) | +| `name` | string or `null` | the [Name][] of the identity if known (eg, for a local [keyring][] identity) | +| `is_self` | boolean | true if the identity is a self-identity, ie, in the local [keyring][] | +| `reachable_broadcast` | boolean | true if the identity is [reachable](#reachable) by broadcast [link](#link) | +| `reachable_unicast` | boolean | true if the identity is [reachable](#reachable) by unicast [link](#link) | +| `reachable_indirect` | boolean | true if the identity is [reachable](#reachable) only via another [node](#node) | +| `interface` | string or `null` | the name of the local network interface on which the identity is reachable | +| `hop_count` | integer | the number of hops to reach the identity | +| `first_hop` | string or `null` | if `hop_count > 1`, then the [SID][] of the first identity in the route | +| `penultimate_hop` | string or `null` | if `hop_count > 2`, then the [SID][] of the penultimate identity in the route | + +----- +**Copyright 2015 Serval Project Inc.** +**Copyright 2016-2018 Flinders University** +![CC-BY-4.0](./cc-by-4.0.png) +Available under the [Creative Commons Attribution 4.0 International licence][CC BY 4.0]. + + +[Serval Project]: http://www.servalproject.org/ +[CC BY 4.0]: ../LICENSE-DOCUMENTATION.md +[Serval Mesh network]: http://developer.servalproject.org/dokuwiki/doku.php?id=content:tech:mesh_network +[Serval DNA]: ../README.md +[REST-API]: ./REST-API.md +[keyring]: ./REST-API-Keyring.md +[SID]: ./REST-API-Keyring.md#serval-id +[DID]: ./REST-API-Keyring.md#did +[Name]: ./REST-API-Keyring.md#name +[unlocked]: ./REST-API-Keyring.md#identity-unlocking +[overlay packet]: ./Mesh-Datagram-Protocol.md#overlay-packet +[sender]: ./Mesh-Datagram-Protocol.md#sender +[transmitter]: ./Mesh-Datagram-Protocol.md#transmitter +[MDP message]: ./Mesh-Datagram-Protocol.md#mdp-message +[broadcast]: ./Mesh-Datagram-Protocol.md#broadcast +[Mesh Datagram Protocol]: ./Mesh-Datagram-Protocol.md +[JSON table]: ./REST-API.md#json-table +[configured]: ./Servald-Configuration.md +[network interface]: ./Servald-Configuration.md#network-interfaces +[200]: ./REST-API.md#200-ok +[201]: ./REST-API.md#201-created +[202]: ./REST-API.md#202-accepted +[400]: ./REST-API.md#400-bad-request +[404]: ./REST-API.md#404-not-found +[419]: ./REST-API.md#419-authentication-timeout +[422]: ./REST-API.md#422-unprocessable-entity +[423]: ./REST-API.md#423-locked +[500]: ./REST-API.md#500-server-error diff --git a/httpd.h b/httpd.h index c95266a1..c07f2ed0 100644 --- a/httpd.h +++ b/httpd.h @@ -23,6 +23,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. #include "http_server.h" #include "keyring.h" +#include "overlay_address.h" #include "meshms.h" #include "os.h" @@ -161,7 +162,7 @@ typedef struct httpd_request */ struct rhizome_read read_state; - /* For responses that list SIDs. + /* For responses that list identities in the keyring. */ struct { enum list_phase phase; @@ -169,6 +170,14 @@ typedef struct httpd_request } sidlist; + /* For responses that list known subscribers, eg, routing table. + */ + struct { + enum list_phase phase; + subscriber_iterator it; + } + subscriberlist; + /* For responses that list manifests. */ struct { diff --git a/keyring_restful.c b/keyring_restful.c index bc90dd1b..960a0e2d 100644 --- a/keyring_restful.c +++ b/keyring_restful.c @@ -152,9 +152,6 @@ static int restful_keyring_identitylist_json_content(struct http_request *hr, un static int restful_keyring_identitylist_json_content_chunk(struct http_request *hr, strbuf b) { httpd_request *r = (httpd_request *) hr; - // The "my_sid" and "their_sid" per-conversation fields allow the same JSON structure to be used - // in a future, non-SID-specific request, eg, to list all conversations for all currently open - // identities. const char *headers[] = { "sid", "identity", diff --git a/overlay_address.c b/overlay_address.c index 908fa728..22bd8289 100644 --- a/overlay_address.c +++ b/overlay_address.c @@ -173,6 +173,33 @@ struct subscriber *find_subscriber(const uint8_t *sidp, int len, int create) return result; } +// iterate over subscribers in ascending binary order + +void subscriber_iterator_start(subscriber_iterator *it) +{ + tree_iterator_start(&it->tree_iterator, &root); +} + +void subscriber_iterator_advance_to(subscriber_iterator *it, const sid_t *sid) +{ + tree_iterator_advance_to(&it->tree_iterator, sid->binary, sizeof sid->binary); +} + +struct subscriber **subscriber_iterator_get_current(subscriber_iterator *it) +{ + return (struct subscriber **) tree_iterator_get_node(&it->tree_iterator); +} + +void subscriber_iterator_advance(subscriber_iterator *it) +{ + tree_iterator_advance(&it->tree_iterator); +} + +void subscriber_iterator_free(subscriber_iterator *it) +{ + tree_iterator_free(&it->tree_iterator); +} + /* walk the tree, starting at start inclusive, calling the supplied callback function */ diff --git a/overlay_address.h b/overlay_address.h index bae173be..2c47fdbf 100644 --- a/overlay_address.h +++ b/overlay_address.h @@ -1,6 +1,7 @@ /* Serval DNA MDP addressing Copyright (C) 2012-2013 Serval Project Inc. +Copyright (C) 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 @@ -125,6 +126,16 @@ extern __thread struct subscriber *directory_service; struct subscriber *find_subscriber(const uint8_t *sid, int len, int create); +typedef struct subscriber_iterator { + tree_iterator tree_iterator; +} subscriber_iterator; + +void subscriber_iterator_start(subscriber_iterator *it); +void subscriber_iterator_advance_to(subscriber_iterator *it, const sid_t *sid); +struct subscriber **subscriber_iterator_get_current(subscriber_iterator *it); +void subscriber_iterator_advance(subscriber_iterator *it); +void subscriber_iterator_free(subscriber_iterator *it); + void enum_subscribers(struct subscriber *start, walk_callback callback, void *context); int set_reachable(struct subscriber *subscriber, struct network_destination *destination, struct subscriber *next_hop, int hop_count, struct subscriber *prior_hop); struct network_destination *load_subscriber_address(struct subscriber *subscriber); diff --git a/route_restful.c b/route_restful.c new file mode 100644 index 00000000..0ae43801 --- /dev/null +++ b/route_restful.c @@ -0,0 +1,179 @@ +/* +Serval DNA Routing HTTP RESTful interface +Copyright (C) 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 +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. +*/ + +#include "lang.h" // for bool_t, FALLTHROUGH +#include "serval.h" +#include "conf.h" +#include "httpd.h" +#include "server.h" +#include "strbuf_helpers.h" +#include "dataformats.h" +#include "overlay_address.h" +#include "overlay_interface.h" + +DEFINE_FEATURE(http_rest_route); + +DECLARE_HANDLER("/restful/route/", restful_route_); + +static HTTP_HANDLER restful_route_list_json; + +static int restful_route_(httpd_request *r, const char *remainder) +{ + r->http.response.header.content_type = &CONTENT_TYPE_JSON; + int ret = authorize_restful(&r->http); + if (ret) + return ret; + if (r->http.verb == HTTP_VERB_GET && strcmp(remainder, "all.json") == 0) + return restful_route_list_json(r, ""); + return 404; +} + +static void finalise_union_subscriberlist(httpd_request *r) +{ + subscriber_iterator_free(&r->u.subscriberlist.it); +} + +static HTTP_CONTENT_GENERATOR restful_route_list_json_content; + +static int restful_route_list_json(httpd_request *r, const char *remainder) +{ + if (*remainder) + return 404; + r->u.subscriberlist.phase = LIST_HEADER; + subscriber_iterator_start(&r->u.subscriberlist.it); + r->finalise_union = finalise_union_subscriberlist; + http_request_response_generated(&r->http, 200, &CONTENT_TYPE_JSON, restful_route_list_json_content); + return 1; +} + +static HTTP_CONTENT_GENERATOR_STRBUF_CHUNKER restful_route_list_json_content_chunk; + +static int restful_route_list_json_content(struct http_request *hr, unsigned char *buf, size_t bufsz, struct http_content_generator_result *result) +{ + return generate_http_content_from_strbuf_chunks(hr, (char *)buf, bufsz, result, restful_route_list_json_content_chunk); +} + +static int restful_route_list_json_content_chunk(struct http_request *hr, strbuf b) +{ + httpd_request *r = (httpd_request *) hr; + const char *headers[] = { + "sid", + "did", + "name", + "is_self", + "reachable_broadcast", + "reachable_unicast", + "reachable_indirect", + "interface", + "hop_count", + "first_hop", + "penultimate_hop" + }; + switch (r->u.subscriberlist.phase) { + case LIST_HEADER: + strbuf_puts(b, "{\n\"header\":["); + unsigned i; + for (i = 0; i != NELS(headers); ++i) { + if (i) + strbuf_putc(b, ','); + strbuf_json_string(b, headers[i]); + } + strbuf_puts(b, "],\n\"rows\":["); + if (!strbuf_overrun(b)){ + r->u.subscriberlist.phase = LIST_FIRST; + if (!subscriber_iterator_get_current(&r->u.subscriberlist.it)) + r->u.subscriberlist.phase = LIST_END; + } + return 1; + + case LIST_ROWS: + strbuf_putc(b, ','); + FALLTHROUGH; + case LIST_FIRST: + r->u.subscriberlist.phase = LIST_ROWS; + struct subscriber **subscriberp = subscriber_iterator_get_current(&r->u.subscriberlist.it); + assert(subscriberp); + struct subscriber *subscriber = *subscriberp; + const char *did = NULL; + const char *name = NULL; + if (subscriber->identity) + keyring_identity_extract(subscriber->identity, &did, &name); + // sid + strbuf_puts(b, "\n["); + strbuf_json_string(b, alloca_tohex_sid_t(subscriber->sid)); + // did + strbuf_puts(b, ","); + strbuf_json_string(b, did); + // name + strbuf_puts(b, ","); + strbuf_json_string(b, name); + // is_self + strbuf_puts(b, ","); + strbuf_json_boolean(b, subscriber->reachable & REACHABLE_SELF); + // reachable_broadcast + strbuf_puts(b, ","); + strbuf_json_boolean(b, subscriber->reachable & REACHABLE_BROADCAST); + // reachable_unicast + strbuf_puts(b, ","); + strbuf_json_boolean(b, subscriber->reachable & REACHABLE_UNICAST); + // reachable_indirect + strbuf_puts(b, ","); + strbuf_json_boolean(b, subscriber->reachable & REACHABLE_INDIRECT); + // interface + strbuf_puts(b, ","); + if (subscriber->destination && subscriber->destination->interface) + strbuf_json_string(b, subscriber->destination->interface->name); + else + strbuf_json_null(b); + // hop_count + strbuf_puts(b, ","); + strbuf_json_integer(b, subscriber->hop_count); + // first_hop + strbuf_puts(b, ","); + if (subscriber->hop_count > 1) + 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) + strbuf_json_string(b, alloca_tohex_sid_t(subscriber->prior_hop->sid)); + else + strbuf_json_null(b); + strbuf_puts(b, "]"); + if (!strbuf_overrun(b)) { + subscriber_iterator_advance(&r->u.subscriberlist.it); + if (!subscriber_iterator_get_current(&r->u.subscriberlist.it)) + r->u.subscriberlist.phase = LIST_END; + } + return 1; + + case LIST_END: + strbuf_puts(b, "\n]\n}\n"); + if (strbuf_overrun(b)) + return 1; + + r->u.subscriberlist.phase = LIST_DONE; + // fall through... + case LIST_DONE: + return 0; + } + abort(); + return 0; +} diff --git a/servald_features.c b/servald_features.c index 8767e3db..f815b497 100644 --- a/servald_features.c +++ b/servald_features.c @@ -54,6 +54,7 @@ void servald_features() USE_FEATURE(http_rhizome); USE_FEATURE(http_rhizome_direct); USE_FEATURE(http_rest_keyring); + USE_FEATURE(http_rest_route); USE_FEATURE(http_rest_rhizome); USE_FEATURE(http_rest_meshms); USE_FEATURE(http_rest_meshmb); diff --git a/sourcefiles.mk b/sourcefiles.mk index 91c2b194..a6c5a4e4 100644 --- a/sourcefiles.mk +++ b/sourcefiles.mk @@ -99,6 +99,7 @@ SERVAL_DAEMON_SOURCES = \ overlay_packetformats.c \ overlay_payload.c \ route_link.c \ + route_restful.c \ rhizome.c \ rhizome_bundle.c \ rhizome_crypto.c \ diff --git a/testdefs.sh b/testdefs.sh index 55223e2b..c78ca439 100644 --- a/testdefs.sh +++ b/testdefs.sh @@ -424,9 +424,9 @@ servald_start() { # Utility function: # - fetch the daemon's primary SID get_servald_primary_sid() { - local _instance="$2" + local _instance="$1" [ -z "$_instance" ] || push_and_set_instance $_instance || return $? - local _var="$1" + local _var="$2" local _sid=$(<"$SERVALINSTANCE_PATH/proc/primary_sid") assert --message="instance $instance_name primary SID is known" [ -n "$_sid" ] if [ -n "$_var" ]; then diff --git a/testdefs_routing.sh b/testdefs_routing.sh new file mode 100644 index 00000000..403e1304 --- /dev/null +++ b/testdefs_routing.sh @@ -0,0 +1,95 @@ +# Common definitions for routing tests. +# +# Copyright 2012-2015 Serval Project, Inc. +# Copyright 2016-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 +# 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. + +interface_is_up() { + $GREP "Interface .* is up" $instance_servald_log || return 1 + return 0 +} + +link_matches() { + local interface_ex=".*" + local link_type="(BROADCAST|UNICAST)" + local via=".*" + while [ $# -ne 0 ]; do + case "$1" in + --interface) interface_ex="$2"; shift 2;; + --broadcast) link_type="BROADCAST"; shift;; + --unicast) link_type="UNICAST"; shift;; + --via) link_type="INDIRECT"; via="$2"; interface_ex=""; shift 2;; + --any) via=".*"; link_type=".*"; shift;; + *) break;; + esac + done + local oIFS="$IFS" + IFS='|' + local sids="$*" + IFS="$oIFS" + local rexp="^(${sids}):${link_type}:${interface_ex}:${via}:" + tfw_log "Looking for $rexp" + if ! $GREP -E "$rexp" "$TFWSTDOUT"; then + tfw_log "Link not found" + tfw_cat --stdout + return 1 + fi +} + +has_link() { + executeOk_servald route print + link_matches $@ +} + +has_no_link() { + has_link --any $@ || return 0 + return 1 +} + +path_exists() { + local dest + eval dest=\$$# + local dest_sidvar=SID${dest#+} + local dest_sids + eval 'dest_sids=("${'$dest_sidvar'[@]}")' + local first_inst=$1 + local next_inst=$first_inst + shift + local I + for I; do + local sidvar=SID${I#+} + local sids + eval 'sids=("${'$sidvar'[@]}")' + [ "${#sids[@]}" -gt 0 ] || error "no SIDs known for identity $I" + set_instance $next_inst + executeOk_servald route print + link_matches "${sids[@]}" || return 1 + [ $I = $dest ] && break + link_matches --via ${!sidvar} "${dest_sids[@]}" || return 1 + next_inst=$I + done + # so we think this path should exist, check that it works + set_instance $first_inst + executeOk_servald --stderr mdp trace --timeout=20 "${dest_sids[0]}" +# assertStdoutGrep "^[0-9]+:$dest_sids\$" + tfw_cat --stdout + return 0 +} + +log_routing_table() { + executeOk_servald route print + tfw_cat --stdout --stderr +} diff --git a/tests/all b/tests/all index fbe1767f..12ec9be3 100755 --- a/tests/all +++ b/tests/all @@ -3,7 +3,7 @@ # Aggregation of all tests except high-load stress 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 @@ -37,10 +37,9 @@ includeTests meshms includeTests meshmb includeTests directory_service includeTests vomp -includeTests keyringrestful -includeTests rhizomerestful -includeTests meshmsrestful -includeTests meshmbrestful + +includeTests allrestful + if type -p "$JAVAC" >/dev/null; then includeTests alljava fi diff --git a/tests/allrestful b/tests/allrestful new file mode 100755 index 00000000..42ced062 --- /dev/null +++ b/tests/allrestful @@ -0,0 +1,30 @@ +#!/bin/bash + +# Aggregation of all REST API tests. +# +# Copyright 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 +# 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" + +includeTests keyringrestful +includeTests routerestful +includeTests rhizomerestful +includeTests meshmsrestful +includeTests meshmbrestful + +runTests "$@" diff --git a/tests/routerestful b/tests/routerestful new file mode 100755 index 00000000..871edf01 --- /dev/null +++ b/tests/routerestful @@ -0,0 +1,125 @@ +#!/bin/bash + +# Tests for Serval DNA Route REST API +# +# Copyright 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 +# 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_rest.sh" +source "${0%/*}/../testdefs_routing.sh" + +setup() { + setup_rest_utilities + setup_servald + assert_no_servald_processes + setup_rest_config +A +} + +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 log.console.level debug \ + set log.console.show_pid on \ + set log.console.show_time on \ + set rhizome.enable no +} + +finally() { + stop_all_servald_servers +} + +teardown() { + foreach_instance_with_pidfile log_routing_table + stop_all_servald_servers + kill_all_servald_processes + assert_no_servald_processes + report_all_servald_servers +} + +doc_AuthBasicMissing="REST API missing Basic Authentication credentials" +setup_AuthBasicMissing() { + setup + set_instance +A + start_servald_server + wait_until_rest_server_ready +} +test_AuthBasicMissing() { + rest_request GET "/restful/route/all.json" 401 --no-auth + assertGrep response.headers "^WWW-Authenticate: Basic realm=\"Serval RESTful API\"$CR\$" + assertJq response.json 'contains({"http_status_code": 401})' + assertJq response.json 'contains({"http_status_message": ""})' +} + +doc_AuthBasicWrong="REST API incorrect Basic Authentication credentials" +setup_AuthBasicWrong() { + setup + set_instance +A + start_servald_server + wait_until_rest_server_ready +} +test_AuthBasicWrong() { + rest_request GET "/restful/route/all.json" 401 --user=fred:nurks + assertGrep response.headers "^WWW-Authenticate: Basic realm=\"Serval RESTful API\"$CR\$" + assertJq response.json 'contains({"http_status_code": 401})' + assertJq response.json 'contains({"http_status_message": ""})' + rest_request GET "/restful/route/all.json" 200 --user=ron:weasley +} + +doc_RouteListAll="REST API list entire routing table" +setup_RouteListAll() { + setup + 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 +} +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'", + "is_self": true, + "hop_count": 0}])' + assertJq routes.json 'contains([{"sid": "'$SIDA2'", + "is_self": true, + "hop_count": 0}])' + 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'", + "is_self": false, + "reachable_unicast": false, + "reachable_indirect": true, + "hop_count": 2}])' + fi + done +} + +runTests "$@" diff --git a/tests/routing b/tests/routing index f22fa3f9..f6676ad3 100755 --- a/tests/routing +++ b/tests/routing @@ -2,7 +2,8 @@ # Tests for Route discovery # -# Copyright 2012 Serval Project, Inc. +# Copyright 2012-2015 Serval Project, Inc. +# Copyright 2016-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 @@ -18,73 +19,9 @@ # 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" - -interface_up() { - $GREP "Interface .* is up" $instance_servald_log || return 1 - return 0 -} - -link_matches() { - local interface_ex=".*" - local link_type="\(BROADCAST\|UNICAST\)" - local via="0*" - local sid="" - while [ $# -ne 0 ]; do - case "$1" in - --interface) interface_ex="$2"; shift 2;; - --broadcast) link_type="BROADCAST"; shift;; - --unicast) link_type="UNICAST"; shift;; - --via) link_type="INDIRECT"; via="$2"; interface_ex=""; shift 2;; - --any) via=".*"; link_type=".*"; shift;; - *) break;; - esac - done - sid="$1" - tfw_log "Looking for link ${sid}, ${link_type}, ${interface_ex}, ${via}" - if ! $GREP "^${sid}:${link_type}:${interface_ex}:${via}:" "$TFWSTDOUT"; then - tfw_log "Link not found" - return 1 - fi -} - -has_link() { - executeOk_servald route print - link_matches $@ -} - -has_no_link() { - has_link --any $@ || return 0 - return 1 -} - -path_exists() { - local dest - eval dest=\$$# - local dest_sidvar=SID${dest#+} - local first_inst=$1 - local next_inst=$first_inst - shift - local I - for I; do - local sidvar=SID${I#+} - [ -n "${!sidvar}" ] || break - set_instance $next_inst - executeOk_servald route print - link_matches ${!sidvar} || return 1 - [ $I = $dest ] && break - link_matches --via ${!sidvar} ${!dest_sidvar} || return 1 - next_inst=$I - done - # so we think this path should exist, check that it works - set_instance $first_inst - executeOk_servald mdp trace --timeout=20 "${!dest_sidvar}" -# assertStdoutGrep "^[0-9]+:${!dest_sidvar}\$" - tfw_cat --stdout - return 0 -} +source "${0%/*}/../testdefs_routing.sh" configure_servald_server() { executeOk_servald config \ @@ -99,11 +36,6 @@ configure_servald_server() { set rhizome.enable no } -log_routing_table() { - executeOk_servald route print - tfw_cat --stdout --stderr -} - teardown() { foreach_instance_with_pidfile log_routing_table stop_all_servald_servers @@ -406,7 +338,7 @@ setup_scan() { set interfaces.1.broadcast.drop on foreach_instance +A +B +C start_servald_server foreach_instance +A +B +C \ - wait_until interface_up + wait_until interface_is_up } test_scan() { set_instance +A @@ -470,7 +402,7 @@ setup_fixedAddress() { touch "$SERVALD_VAR/dummy1" foreach_instance +A +B start_servald_server foreach_instance +A +B \ - wait_until interface_up + wait_until interface_is_up } doc_fixedAddress="Establish a link from a configured fixed address" test_fixedAddress() {