From 11e9d382995da9fc2c0342abcc52623cc8a80781 Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Thu, 26 Jun 2014 16:25:19 +0930 Subject: [PATCH 01/21] Rename config 'rhizome.api.restful' to 'api.restful' --- conf_schema.h | 36 ++++++++++--------- httpd.c | 6 ++-- .../servaldna/ServerControl.java | 4 +-- meshms_restful.c | 4 +-- rhizome_restful.c | 4 +-- tests/meshmsrestful | 14 ++++---- tests/rhizomerestful | 10 +++--- 7 files changed, 41 insertions(+), 37 deletions(-) diff --git a/conf_schema.h b/conf_schema.h index 52b701e6..15f49adb 100644 --- a/conf_schema.h +++ b/conf_schema.h @@ -384,15 +384,6 @@ STRUCT(rhizome_direct) SUB_STRUCT(peerlist, peer,) END_STRUCT -STRUCT(user) -STRING(50, password, "", str,, "Authentication password") -END_STRUCT - -ARRAY(userlist,) -KEY_STRING(25, str) -VALUE_SUB_STRUCT(user) -END_ARRAY(10) - STRUCT(rhizome_api_addfile) STRING(64, uri_path, "", absolute_path,, "URI path for HTTP add-file request") ATOM(struct in_addr, allow_host, hton_in_addr(INADDR_LOOPBACK), in_addr,, "IP address of host allowed to make HTTP add-file request") @@ -401,15 +392,8 @@ ATOM(sid_t, default_author, SID_ANY, sid,, "Author of ad ATOM(rhizome_bk_t, bundle_secret_key, RHIZOME_BK_NONE, rhizome_bk,, "Secret key of add-file bundle to try if sender not given") END_STRUCT -STRUCT(rhizome_api_restful) -SUB_STRUCT(userlist, users,) -ATOM(uint32_t, newsince_timeout, 60, uint32_time_interval,, "Time to block while reporting new bundles") -ATOM(uint32_t, newsince_poll_ms, 2000, uint32_nonzero,, "Database poll interval while blocked reporting new bundles") -END_STRUCT - STRUCT(rhizome_api) SUB_STRUCT(rhizome_api_addfile, addfile,) -SUB_STRUCT(rhizome_api_restful, restful,) END_STRUCT STRUCT(rhizome_http) @@ -490,6 +474,25 @@ KEY_ATOM(unsigned, uint) VALUE_NODE_STRUCT(network_interface, network_interface) END_ARRAY(10) +STRUCT(user) +STRING(50, password, "", str,, "Authentication password") +END_STRUCT + +ARRAY(userlist,) +KEY_STRING(25, str) +VALUE_SUB_STRUCT(user) +END_ARRAY(10) + +STRUCT(api_restful) +SUB_STRUCT(userlist, users,) +ATOM(uint32_t, newsince_timeout, 60, uint32_time_interval,, "Time to block while reporting new bundles") +ATOM(uint32_t, newsince_poll_ms, 2000, uint32_nonzero,, "Database poll interval while blocked reporting new bundles") +END_STRUCT + +STRUCT(api) +SUB_STRUCT(api_restful, restful,) +END_STRUCT + // The top level. STRUCT(main) NODE_STRUCT(interface_list, interfaces, interface_list,) @@ -504,4 +507,5 @@ SUB_STRUCT(rhizome, rhizome,) SUB_STRUCT(directory, directory,) SUB_STRUCT(olsr, olsr,) SUB_STRUCT(host_list, hosts,) +SUB_STRUCT(api, api,) END_STRUCT diff --git a/httpd.c b/httpd.c index 52e86271..97d0f7d1 100644 --- a/httpd.c +++ b/httpd.c @@ -333,9 +333,9 @@ static int is_authorized(const struct http_client_authorization *auth) if (auth->scheme != BASIC) return 0; unsigned i; - for (i = 0; i != config.rhizome.api.restful.users.ac; ++i) { - if ( strcmp(config.rhizome.api.restful.users.av[i].key, auth->credentials.basic.user) == 0 - && strcmp(config.rhizome.api.restful.users.av[i].value.password, auth->credentials.basic.password) == 0 + for (i = 0; i != config.api.restful.users.ac; ++i) { + if ( strcmp(config.api.restful.users.av[i].key, auth->credentials.basic.user) == 0 + && strcmp(config.api.restful.users.av[i].value.password, auth->credentials.basic.password) == 0 ) return 1; } diff --git a/java/org/servalproject/servaldna/ServerControl.java b/java/org/servalproject/servaldna/ServerControl.java index 30a6cdc4..902eec9e 100644 --- a/java/org/servalproject/servaldna/ServerControl.java +++ b/java/org/servalproject/servaldna/ServerControl.java @@ -99,11 +99,11 @@ public class ServerControl { * 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"); + String restfulPassword = ServalDCommand.getConfigItem("api.restful.users." + restfulUsername + ".password"); if (restfulPassword == null) { restfulPassword = new BigInteger(130, new SecureRandom()).toString(32); ServalDCommand.configActions( - ServalDCommand.ConfigAction.set, "rhizome.api.restful.users." + restfulUsername + ".password", restfulPassword, + ServalDCommand.ConfigAction.set, "api.restful.users." + restfulUsername + ".password", restfulPassword, ServalDCommand.ConfigAction.sync ); } diff --git a/meshms_restful.c b/meshms_restful.c index bffd57f0..25a18e09 100644 --- a/meshms_restful.c +++ b/meshms_restful.c @@ -341,7 +341,7 @@ static int restful_meshms_newsince_messagelist_json(httpd_request *r, const char return 404; } r->u.msglist.token_offset = r->ui64; - r->u.msglist.end_time = gettime_ms() + config.rhizome.api.restful.newsince_timeout * 1000; + r->u.msglist.end_time = gettime_ms() + config.api.restful.newsince_timeout * 1000; http_request_response_generated(&r->http, 200, CONTENT_TYPE_JSON, restful_meshms_messagelist_json_content); return 1; } @@ -402,7 +402,7 @@ static int restful_meshms_messagelist_json_content_chunk(struct http_request *hr r->u.msglist.token_which_ply = r->u.msglist.latest_which_ply; r->u.msglist.token_offset = r->u.msglist.latest_offset; meshms_message_iterator_close(&r->u.msglist.iter); - time_ms_t wake_at = now + config.rhizome.api.restful.newsince_poll_ms; + time_ms_t wake_at = now + config.api.restful.newsince_poll_ms; if (wake_at > r->u.msglist.end_time) wake_at = r->u.msglist.end_time; http_request_pause_response(&r->http, wake_at); diff --git a/rhizome_restful.c b/rhizome_restful.c index baa9863d..2ffcdecc 100644 --- a/rhizome_restful.c +++ b/rhizome_restful.c @@ -115,7 +115,7 @@ int restful_rhizome_newsince(httpd_request *r, const char *remainder) r->u.rhlist.rowcount = 0; bzero(&r->u.rhlist.cursor, sizeof r->u.rhlist.cursor); r->u.rhlist.cursor.rowid_since = rowid; - r->u.rhlist.end_time = gettime_ms() + config.rhizome.api.restful.newsince_timeout * 1000; + r->u.rhlist.end_time = gettime_ms() + config.api.restful.newsince_timeout * 1000; http_request_response_generated(&r->http, 200, CONTENT_TYPE_JSON, restful_rhizome_bundlelist_json_content); return 1; } @@ -163,7 +163,7 @@ static int restful_rhizome_bundlelist_json_content_chunk(struct http_request *hr r->u.rhlist.phase = LIST_END; return 1; } - time_ms_t wake_at = now + config.rhizome.api.restful.newsince_poll_ms; + time_ms_t wake_at = now + config.api.restful.newsince_poll_ms; if (wake_at > r->u.rhlist.end_time) wake_at = r->u.rhlist.end_time; http_request_pause_response(&r->http, wake_at); diff --git a/tests/meshmsrestful b/tests/meshmsrestful index 64cbb325..967c504b 100755 --- a/tests/meshmsrestful +++ b/tests/meshmsrestful @@ -33,9 +33,9 @@ setup() { set_instance +A set_meshms_config executeOk_servald config \ - set rhizome.api.restful.users.harry.password potter \ - set rhizome.api.restful.users.ron.password weasley \ - set rhizome.api.restful.users.hermione.password grainger + set api.restful.users.harry.password potter \ + set api.restful.users.ron.password weasley \ + set api.restful.users.hermione.password grainger set_extra_config if [ -z "$IDENTITY_COUNT" ]; then create_single_identity @@ -288,8 +288,8 @@ doc_MeshmsListMessagesNewSince="HTTP RESTful list MeshMS messages in one convers setup_MeshmsListMessagesNewSince() { IDENTITY_COUNT=2 set_extra_config() { - executeOk_servald config set rhizome.api.restful.newsince_timeout 1s \ - set rhizome.api.restful.newsince_poll_ms 500 + executeOk_servald config set api.restful.newsince_timeout 1s \ + set api.restful.newsince_poll_ms 500 } setup meshms_add_messages $SIDA1 $SIDA2 '><>>A>A<>><><><>>>A>A><<<><>>A<<>' @@ -340,8 +340,8 @@ doc_MeshmsListMessagesNewSinceArrival="HTTP RESTful list newly arriving MeshMS m setup_MeshmsListMessagesNewSinceArrival() { IDENTITY_COUNT=2 set_extra_config() { - executeOk_servald config set rhizome.api.restful.newsince_timeout 60s \ - set rhizome.api.restful.newsince_poll_ms 500 + executeOk_servald config set api.restful.newsince_timeout 60s \ + set api.restful.newsince_poll_ms 500 } setup meshms_add_messages $SIDA1 $SIDA2 '><>A>' diff --git a/tests/rhizomerestful b/tests/rhizomerestful index 180aea31..e007ba66 100755 --- a/tests/rhizomerestful +++ b/tests/rhizomerestful @@ -34,9 +34,9 @@ setup() { set_instance +A set_rhizome_config executeOk_servald config \ - set rhizome.api.restful.users.harry.password potter \ - set rhizome.api.restful.users.ron.password weasley \ - set rhizome.api.restful.users.hermione.password grainger + set api.restful.users.harry.password potter \ + set api.restful.users.ron.password weasley \ + set api.restful.users.hermione.password grainger set_extra_config if [ -z "$IDENTITY_COUNT" ]; then create_single_identity @@ -201,8 +201,8 @@ test_RhizomeList() { doc_RhizomeNewSince="HTTP RESTful list Rhizome bundles since token as JSON" setup_RhizomeNewSince() { set_extra_config() { - executeOk_servald config set rhizome.api.restful.newsince_timeout 60s \ - set rhizome.api.restful.newsince_poll_ms 500 + executeOk_servald config set api.restful.newsince_timeout 60s \ + set api.restful.newsince_poll_ms 500 } setup add_bundles 0 5 From 067340bbba734c8b505a94815798f5a1ddd5283a Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Thu, 26 Jun 2014 16:36:25 +0930 Subject: [PATCH 02/21] Change HTTP Authorization realm to "Serval RESTful API" Was "Serval Rhizome", which is not accurate --- httpd.c | 8 ++++---- httpd.h | 2 +- meshms_restful.c | 2 +- rhizome_restful.c | 8 ++++---- tests/meshmsrestful | 4 ++-- tests/rhizomerestful | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/httpd.c b/httpd.c index 97d0f7d1..b462b0ae 100644 --- a/httpd.c +++ b/httpd.c @@ -328,7 +328,7 @@ static int is_from_loopback(const struct http_request *r) /* Return 1 if the given authorization credentials are acceptable. * Return 0 if not. */ -static int is_authorized(const struct http_client_authorization *auth) +static int is_authorized_restful(const struct http_client_authorization *auth) { if (auth->scheme != BASIC) return 0; @@ -342,13 +342,13 @@ static int is_authorized(const struct http_client_authorization *auth) return 0; } -int authorize(struct http_request *r) +int authorize_restful(struct http_request *r) { if (!is_from_loopback(r)) return 403; - if (!is_authorized(&r->request_header.authorization)) { + if (!is_authorized_restful(&r->request_header.authorization)) { r->response.header.www_authenticate.scheme = BASIC; - r->response.header.www_authenticate.realm = "Serval Rhizome"; + r->response.header.www_authenticate.realm = "Serval RESTful API"; return 401; } return 0; diff --git a/httpd.h b/httpd.h index 8848e687..76cca441 100644 --- a/httpd.h +++ b/httpd.h @@ -191,7 +191,7 @@ int httpd_server_start(uint16_t port_low, uint16_t port_high); typedef int HTTP_HANDLER(httpd_request *r, const char *remainder); int is_http_header_complete(const char *buf, size_t len, size_t read_since_last_call); -int authorize(struct http_request *r); +int authorize_restful(struct http_request *r); int http_response_content_type(httpd_request *r, const char *what, const struct mime_content_type *ct); int http_response_content_disposition(httpd_request *r, const char *what, const char *type); int http_response_form_part(httpd_request *r, const char *what, const char *partname, const char *text, size_t textlen); diff --git a/meshms_restful.c b/meshms_restful.c index 25a18e09..ae3149f7 100644 --- a/meshms_restful.c +++ b/meshms_restful.c @@ -185,7 +185,7 @@ int restful_meshms_(httpd_request *r, const char *remainder) http_request_simple_response(&r->http, 400, "Bad content length"); return 400; } - int ret = authorize(&r->http); + int ret = authorize_restful(&r->http); if (ret) return ret; ret = handler(r, remainder); diff --git a/rhizome_restful.c b/rhizome_restful.c index 2ffcdecc..5a178e36 100644 --- a/rhizome_restful.c +++ b/rhizome_restful.c @@ -74,7 +74,7 @@ int restful_rhizome_bundlelist_json(httpd_request *r, const char *remainder) return 404; if (r->http.verb != HTTP_VERB_GET) return 405; - int ret = authorize(&r->http); + int ret = authorize_restful(&r->http); if (ret) return ret; r->u.rhlist.phase = LIST_HEADER; @@ -108,7 +108,7 @@ int restful_rhizome_newsince(httpd_request *r, const char *remainder) return 404; if (r->http.verb != HTTP_VERB_GET) return 405; - int ret = authorize(&r->http); + int ret = authorize_restful(&r->http); if (ret) return ret; r->u.rhlist.phase = LIST_HEADER; @@ -248,7 +248,7 @@ int restful_rhizome_insert(httpd_request *r, const char *remainder) return 403; if (r->http.verb != HTTP_VERB_POST) return 405; - int ret = authorize(&r->http); + int ret = authorize_restful(&r->http); if (ret) return ret; // Parse the request body as multipart/form-data. @@ -589,7 +589,7 @@ int restful_rhizome_(httpd_request *r, const char *remainder) return 404; if (r->http.verb != HTTP_VERB_GET) return 405; - int ret = authorize(&r->http); + int ret = authorize_restful(&r->http); if (ret) return ret; if ((r->manifest = rhizome_new_manifest()) == NULL) diff --git a/tests/meshmsrestful b/tests/meshmsrestful index 967c504b..128b3442 100755 --- a/tests/meshmsrestful +++ b/tests/meshmsrestful @@ -81,7 +81,7 @@ test_AuthBasicMissing() { --dump-header http.headers \ "http://$addr_localhost:$PORTA/restful/meshms/$SIDA/conversationlist.json" assertStdoutIs '401' - assertGrep http.headers "^WWW-Authenticate: Basic realm=\"Serval Rhizome\"$CR\$" + assertGrep http.headers "^WWW-Authenticate: Basic realm=\"Serval RESTful API\"$CR\$" assertJq http.output 'contains({"http_status_code": 401})' assertJq http.output 'contains({"http_status_message": ""})' } @@ -99,7 +99,7 @@ test_AuthBasicWrong() { --basic --user fred:nurks \ "http://$addr_localhost:$PORTA/restful/meshms/$SIDA/conversationlist.json" assertStdoutIs '401' - assertGrep http.headers "^WWW-Authenticate: Basic realm=\"Serval Rhizome\"$CR\$" + assertGrep http.headers "^WWW-Authenticate: Basic realm=\"Serval RESTful API\"$CR\$" assertJq http.output 'contains({"http_status_code": 401})' assertJq http.output 'contains({"http_status_message": ""})' executeOk curl \ diff --git a/tests/rhizomerestful b/tests/rhizomerestful index e007ba66..b99b6285 100755 --- a/tests/rhizomerestful +++ b/tests/rhizomerestful @@ -81,7 +81,7 @@ test_AuthBasicMissing() { --dump-header http.headers \ "http://$addr_localhost:$PORTA/restful/rhizome/bundlelist.json" assertStdoutIs '401' - assertGrep http.headers "^WWW-Authenticate: Basic realm=\"Serval Rhizome\"$CR\$" + assertGrep http.headers "^WWW-Authenticate: Basic realm=\"Serval RESTful API\"$CR\$" assertJq http.output 'contains({"http_status_code": 401})' assertJq http.output 'contains({"http_status_message": ""})' } @@ -99,7 +99,7 @@ test_AuthBasicWrong() { --basic --user fred:nurks \ "http://$addr_localhost:$PORTA/restful/rhizome/bundlelist.json" assertStdoutIs '401' - assertGrep http.headers "^WWW-Authenticate: Basic realm=\"Serval Rhizome\"$CR\$" + assertGrep http.headers "^WWW-Authenticate: Basic realm=\"Serval RESTful API\"$CR\$" assertJq http.output 'contains({"http_status_code": 401})' assertJq http.output 'contains({"http_status_message": ""})' executeOk curl \ From 188a67d3c152aae8c8d94526c74e5980150f7ffa Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Fri, 27 Jun 2014 11:11:29 +0930 Subject: [PATCH 03/21] Improve test framework: tfw_shopt, tfs_shopt_restore --- testframework.sh | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/testframework.sh b/testframework.sh index f3567360..dae07c3f 100644 --- a/testframework.sh +++ b/testframework.sh @@ -99,13 +99,13 @@ Options: " } -# Internal utility for setting shopt variables and restoring their original +# Utility functions for setting shopt variables and restoring their original # value: # local oo -# _tfw_shopt oo -s extglob -u extdebug +# tfw_shopt oo -s extglob -u extdebug # ... -# _tfw_shopt_restore oo -_tfw_shopt() { +# tfw_shopt_restore oo +tfw_shopt() { local _var="$1" shift local op=s @@ -125,7 +125,7 @@ _tfw_shopt() { done eval $_var='"$restore"' } -_tfw_shopt_restore() { +tfw_shopt_restore() { local _var="$1" [ -n "${!_var}" ] && eval "${!_var}" } @@ -138,7 +138,7 @@ declare -a _tfw_forked_pids=() declare -a _tfw_forked_labels=() # The rest of this file is parsed for extended glob patterns. -_tfw_shopt _tfw_orig_shopt -s extglob +tfw_shopt _tfw_orig_shopt -s extglob includeTests() { local arg @@ -190,7 +190,7 @@ runTests() { local allargs="$*" local -a filters=() local oo - _tfw_shopt oo -s extglob + tfw_shopt oo -s extglob while [ $# -ne 0 ]; do case "$1" in -h|--help) usage; exit 0;; @@ -242,7 +242,7 @@ runTests() { esac shift done - _tfw_shopt_restore oo + tfw_shopt_restore oo if $_tfw_verbose && [ $_tfw_njobs -ne 1 ]; then _tfw_fatal "--verbose is incompatible with --jobs=$_tfw_njobs" fi @@ -967,7 +967,7 @@ _tfw_getopts() { _tfw_opt_grepopts=() _tfw_getopts_shift=0 local oo - _tfw_shopt oo -s extglob + tfw_shopt oo -s extglob while [ $# -ne 0 ]; do case "$context:$1" in *:--stdout) _tfw_dump_on_fail --stdout;; @@ -1014,29 +1014,29 @@ _tfw_getopts() { [ -z "$_tfw_executable" ] && _tfw_error "missing executable argument" ;; esac - _tfw_shopt_restore oo + tfw_shopt_restore oo return 0 } _tfw_is_uint() { local oo - _tfw_shopt oo -s extglob + tfw_shopt oo -s extglob local ret=1 case "$1" in +([0-9])) ret=0;; esac - _tfw_shopt_restore oo + tfw_shopt_restore oo return $ret } _tfw_is_float() { local oo - _tfw_shopt oo -s extglob + tfw_shopt oo -s extglob local ret=1 case "$1" in @(+([0-9])?(.+([0-9]))|*([0-9]).+([0-9]))) ret=0;; esac - _tfw_shopt_restore oo + tfw_shopt_restore oo return $ret } @@ -1173,7 +1173,7 @@ _tfw_assert_grep() { local ret=0 local info="$matches match"$([ $matches -ne 1 ] && echo "es") local oo - _tfw_shopt oo -s extglob + tfw_shopt oo -s extglob case "$_tfw_opt_matches" in '') done=true @@ -1231,7 +1231,7 @@ _tfw_assert_grep() { _tfw_error "unsupported value for --matches=$_tfw_opt_matches" ret=$? fi - _tfw_shopt_restore oo + tfw_shopt_restore oo fi if [ $ret -ne 0 ]; then _tfw_backtrace @@ -1310,7 +1310,7 @@ _tfw_unpack_words() { _tfw_find_tests() { ( local oo - _tfw_shopt oo -s extdebug + tfw_shopt oo -s extdebug local func for func in $(builtin declare -F | $SED -n -e '/^declare -f test_[A-Za-z]/s/^declare -f //p'); do local funcname @@ -1337,7 +1337,7 @@ _tfw_find_tests() { echo $lineno $number $name "$path" done done - _tfw_shopt_restore oo + tfw_shopt_restore oo ) | sort -n -k1 -k2 | $SED -e 's/^[0-9][0-9]* [0-9][0-9]* //' } @@ -1369,7 +1369,7 @@ _tfw_filter_predicate() { local -a filters=("$@") local ret=1 local oo - _tfw_shopt oo -s extglob + tfw_shopt oo -s extglob if [ ${#filters[*]} -eq 0 ]; then ret=0 else @@ -1425,7 +1425,7 @@ _tfw_filter_predicate() { esac done fi - _tfw_shopt_restore oo + tfw_shopt_restore oo return $ret } @@ -1966,7 +1966,7 @@ fork_wait_all() { _tfw_set_forklabel() { local oo - _tfw_shopt oo -s extglob + tfw_shopt oo -s extglob local ret=1 case "$1" in '%'+([[A-Za-z0-9])) @@ -1979,7 +1979,7 @@ _tfw_set_forklabel() { ret=0 ;; esac - _tfw_shopt_restore oo + tfw_shopt_restore oo return $ret } @@ -2191,4 +2191,4 @@ escape_grep_extended() { } # Restore the caller's shopt preferences before returning. -_tfw_shopt_restore _tfw_orig_shopt +tfw_shopt_restore _tfw_orig_shopt From c5d3069d823f52494ed5095529ab60fbdab0a8fd Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Fri, 27 Jun 2014 17:26:01 +0930 Subject: [PATCH 04/21] 'make all' now makes libserval.so --- Makefile.in | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Makefile.in b/Makefile.in index 5a037ee0..984acc72 100644 --- a/Makefile.in +++ b/Makefile.in @@ -64,7 +64,7 @@ DEFS= @DEFS@ .PHONY: all test clean -all: servald libmonitorclient.so libmonitorclient.a test +all: servald libserval.so libmonitorclient.so libmonitorclient.a test test: tfw_createfile directory_service fakeradio config_test simulator @@ -196,8 +196,6 @@ copyright: findPATH = $(firstword $(wildcard $(addsuffix /$(1),$(subst :, ,$(PATH))))) COPYRIGHT_TOOL := $(call findPATH,sp-copyright-tool) -# This does not build on 64 bit elf platforms as NaCL isn't built with -fPIC -# DOC 20120615 libserval.so: $(SERVALD_OBJS) $(OBJSDIR_TOOLS)/version.o @echo LINK $@ @$(CC) -Wall -shared -o $@ $(SERVALD_OBJS) $(OBJSDIR_TOOLS)/version.o $(LDFLAGS) From a9ec5dcf695ece8363f838fc71cd25f92a1ed0bc Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Fri, 27 Jun 2014 17:29:29 +0930 Subject: [PATCH 05/21] Refactor Rhizome test script ready for re-use Move add_bundles() function from 'rhizomerestful' test script into testdefs_rhizome.sh, rename to rhizome_add_bundles() and add a new SID argument --- testdefs_rhizome.sh | 43 +++++++++++++++++++++++++++++++++- tests/rhizomerestful | 55 +++++++------------------------------------- 2 files changed, 50 insertions(+), 48 deletions(-) diff --git a/testdefs_rhizome.sh b/testdefs_rhizome.sh index 09f81820..98a4ac91 100644 --- a/testdefs_rhizome.sh +++ b/testdefs_rhizome.sh @@ -1,5 +1,5 @@ # Common definitions for Rhizome test suites. -# Copyright 2012 Serval Project Inc. +# Copyright 2012-2014 Serval Project Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -511,3 +511,44 @@ assert_rhizome_received() { fi done } + +rhizome_add_bundles() { + local encrypted=false + case "$1" in + --encrypted) encrypted=true; shift;; + esac + local SID="${1?}" + shift + local n + for ((n = $1; n <= $2; ++n)); do + create_file file$n $((1000 + $n)) + if $encrypted; then + echo "crypt=1" >file$n.manifest + fi + executeOk_servald rhizome add file $SID file$n file$n.manifest + extract_stdout_manifestid BID[$n] + extract_stdout_version VERSION[$n] + extract_stdout_filesize SIZE[$n] + extract_stdout_filehash HASH[$n] + extract_stdout_date DATE[$n] + extract_stdout_BK BK[$n] + extract_stdout_rowid ROWID[$n] + extract_stdout_author AUTHOR[$n] + extract_stdout_secret SECRET[$n] + extract_stdout_inserttime INSERTTIME[$n] + NAME[$n]=file$n + if $encrypted; then + extract_stdout_crypt CRYPT[$n] + assert [ "${CRYPT[$n]}" = 1 ] + else + CRYPT[$n]= + fi + executeOk_servald rhizome export file ${HASH[$n]} raw$n + if $encrypted; then + assert ! cmp file$n raw$n + else + assert cmp file$n raw$n + fi + [ "${ROWID[$n]}" -gt "${ROWID_MAX:-0}" ] && ROWID_MAX=${ROWID[$n]} + done +} diff --git a/tests/rhizomerestful b/tests/rhizomerestful index b99b6285..1c8c3944 100755 --- a/tests/rhizomerestful +++ b/tests/rhizomerestful @@ -115,50 +115,11 @@ teardown_AuthBasicWrong() { teardown } -add_bundles() { - local encrypted=false - case "$1" in - --encrypted) encrypted=true; shift;; - esac - local n - for ((n = $1; n <= $2; ++n)); do - create_file file$n $((1000 + $n)) - if $encrypted; then - echo "crypt=1" >file$n.manifest - fi - executeOk_servald rhizome add file $SIDA file$n file$n.manifest - extract_stdout_manifestid BID[$n] - extract_stdout_version VERSION[$n] - extract_stdout_filesize SIZE[$n] - extract_stdout_filehash HASH[$n] - extract_stdout_date DATE[$n] - extract_stdout_BK BK[$n] - extract_stdout_rowid ROWID[$n] - extract_stdout_author AUTHOR[$n] - extract_stdout_secret SECRET[$n] - extract_stdout_inserttime INSERTTIME[$n] - NAME[$n]=file$n - if $encrypted; then - extract_stdout_crypt CRYPT[$n] - assert [ "${CRYPT[$n]}" = 1 ] - else - CRYPT[$n]= - fi - executeOk_servald rhizome export file ${HASH[$n]} raw$n - if $encrypted; then - assert ! cmp file$n raw$n - else - assert cmp file$n raw$n - fi - [ "${ROWID[$n]}" -gt "${ROWID_MAX:-0}" ] && ROWID_MAX=${ROWID[$n]} - done -} - doc_RhizomeList="HTTP RESTful list Rhizome bundles as JSON" setup_RhizomeList() { setup NBUNDLES=100 - add_bundles 0 $((NBUNDLES-1)) + rhizome_add_bundles $SIDA 0 $((NBUNDLES-1)) assert [ "$ROWID_MAX" -ge "$NBUNDLES" ] } test_RhizomeList() { @@ -205,7 +166,7 @@ setup_RhizomeNewSince() { set api.restful.newsince_poll_ms 500 } setup - add_bundles 0 5 + rhizome_add_bundles $SIDA 0 5 executeOk curl \ --silent --fail --show-error \ --output bundlelist.json \ @@ -227,7 +188,7 @@ test_RhizomeNewSince() { "http://$addr_localhost:$PORTA/restful/rhizome/newsince/$token/bundlelist.json" done wait_until [ -e newsince1.json -a -e newsince2.json -a -e newsince3.json ] - add_bundles 6 10 + rhizome_add_bundles $SIDA 6 10 for i in 1 2 3; do wait_until --timeout=10 grep "${BID[10]}" newsince$i.json done @@ -282,7 +243,7 @@ assert_http_response_headers() { doc_RhizomeManifest="HTTP RESTful fetch Rhizome manifest" setup_RhizomeManifest() { setup - add_bundles 0 2 + rhizome_add_bundles $SIDA 0 2 } test_RhizomeManifest() { for n in 0 1 2; do @@ -304,8 +265,8 @@ test_RhizomeManifest() { doc_RhizomePayloadRaw="HTTP RESTful fetch Rhizome raw payload" setup_RhizomePayloadRaw() { setup - add_bundles 0 1 - add_bundles --encrypted 2 3 + rhizome_add_bundles $SIDA 0 1 + rhizome_add_bundles --encrypted $SIDA 2 3 } test_RhizomePayloadRaw() { for n in 0 1 2 3; do @@ -326,8 +287,8 @@ test_RhizomePayloadRaw() { doc_RhizomePayloadDecrypted="HTTP RESTful fetch Rhizome decrypted payload" setup_RhizomePayloadDecrypted() { setup - add_bundles 0 1 - add_bundles --encrypted 2 3 + rhizome_add_bundles $SIDA 0 1 + rhizome_add_bundles --encrypted $SIDA 2 3 } test_RhizomePayloadDecrypted() { for n in 0 1 2 3; do From 1ac67de0e9407cd827f87535c66f8144f34bcec3 Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Thu, 10 Jul 2014 05:16:02 +0930 Subject: [PATCH 06/21] Fix bug in HTTP server's form-data parsing Did not handle a non-empty preamble properly --- http_server.c | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/http_server.c b/http_server.c index 5fbb2fe9..298b9ba8 100644 --- a/http_server.c +++ b/http_server.c @@ -342,6 +342,14 @@ static inline void _commit(struct http_request *r) r->parsed = r->cursor; } +static inline int _skip_any(struct http_request *r) +{ + if (_run_out(r)) + return 0; + ++r->cursor; + return 1; +} + static inline void _skip_all(struct http_request *r) { r->cursor = r->end; @@ -1249,16 +1257,21 @@ static int http_request_parse_body_form_data(struct http_request *r) if (config.debug.http_server) DEBUGF("PREAMBLE"); char *start = r->parsed; - for (; at_start || _skip_to_crlf(r); at_start = 0) { - const char *end_preamble = r->cursor; + while (at_start || _skip_to_crlf(r)) { + char *end_preamble = r->cursor; int b; - if ((b = _skip_mime_boundary(r))) { + if ((at_start || _skip_crlf(r)) && (b = _skip_mime_boundary(r))) { assert(end_preamble >= r->parsed); - _INVOKE_HANDLER_BUF_LEN(handle_mime_preamble, r->parsed, end_preamble); + _INVOKE_HANDLER_BUF_LEN(handle_mime_preamble, start, end_preamble); _rewind_crlf(r); _commit(r); return http_request_form_data_start_part(r, b); } + if (!at_start) { + r->cursor = end_preamble; + _skip_any(r); + } + at_start = 0; } if (_end_of_content(r)) { if (r->debug_flag && *r->debug_flag) From 61023287b16e25d7a4e504ddc0815ed2125f1943 Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Thu, 10 Jul 2014 05:19:38 +0930 Subject: [PATCH 07/21] Refactor Rhizome manifest validation New function returns text describing the failed validation --- rhizome.h | 5 ++- rhizome_bundle.c | 113 +++++++++++++++++++++-------------------------- 2 files changed, 53 insertions(+), 65 deletions(-) diff --git a/rhizome.h b/rhizome.h index a2c78aad..5c9a2b46 100644 --- a/rhizome.h +++ b/rhizome.h @@ -108,7 +108,7 @@ typedef struct rhizome_manifest unsigned char *signatories[MAX_MANIFEST_VARS]; uint8_t signatureTypes[MAX_MANIFEST_VARS]; - /* Set to non-zero if a manifest has been parsed that cannot be fully + /* Set to non-NULL if a manifest has been parsed that cannot be fully * understood by this version of Rhizome (probably from a future or a very * old past version of Rhizome). During add (local injection), the manifest * should not be imported. During extract (local decode) a warning or error @@ -116,7 +116,7 @@ typedef struct rhizome_manifest * transported, imported and exported normally, as long as their signature is * valid. */ - unsigned short malformed; + const char *malformed; /* Set non-zero after variables have been packed and signature blocks * appended. All fields below may not be valid until the manifest has been @@ -381,6 +381,7 @@ int rhizome_write_manifest_file(rhizome_manifest *m, const char *filename, char int rhizome_manifest_selfsign(rhizome_manifest *m); int rhizome_read_manifest_from_file(rhizome_manifest *m, const char *filename); int rhizome_manifest_validate(rhizome_manifest *m); +const char *rhizome_manifest_validate_reason(rhizome_manifest *m); int rhizome_manifest_parse(rhizome_manifest *m); int rhizome_manifest_verify(rhizome_manifest *m); diff --git a/rhizome_bundle.c b/rhizome_bundle.c index 1a4563d9..7822ccbc 100644 --- a/rhizome_bundle.c +++ b/rhizome_bundle.c @@ -427,7 +427,7 @@ static void rhizome_manifest_clear(rhizome_manifest *m) free(m->signatories[m->sig_count]); m->signatories[m->sig_count] = NULL; } - m->malformed = 0; + m->malformed = NULL; m->has_id = 0; m->has_filehash = 0; m->is_journal = 0; @@ -542,7 +542,7 @@ int rhizome_manifest_parse(rhizome_manifest *m) assert(m->manifest_body_bytes == 0); assert(m->var_count == 0); assert(!m->finalised); - assert(!m->malformed); + assert(m->malformed == NULL); assert(!m->has_id); assert(!m->has_filehash); assert(!m->is_journal); @@ -742,11 +742,11 @@ int rhizome_manifest_parse(rhizome_manifest *m) reason = "invalid"; break; case FIELD_UNKNOWN: - ++m->malformed; + m->malformed = "Unsupported field"; reason = "unsupported"; break; case FIELD_MALFORMED: - ++m->malformed; + m->malformed = "Invalid field"; reason = "invalid"; break; default: @@ -778,75 +778,62 @@ int rhizome_manifest_parse(rhizome_manifest *m) /* If all essential (transport) fields are present and well formed then sets the m->finalised field * and returns 1, otherwise returns 0. * - * Increments m->malformed if any non-essential fields are missing or invalid. It is up to the - * caller to check the m->malformed field and decide whether or not to process a malformed manifest. + * Sets m->malformed if any non-essential fields are missing or invalid. It is up to the caller to + * check m->malformed and decide whether or not to process a malformed manifest. * * @author Andrew Bettison */ int rhizome_manifest_validate(rhizome_manifest *m) { - int ret = 1; - if (!m->has_id) { - if (config.debug.rhizome_manifest) - DEBUG("Missing 'id' field"); - ret = 0; - } - if (m->version == 0) { - if (config.debug.rhizome_manifest) - DEBUG("Missing 'version' field"); - ret = 0; - } - if (m->filesize == RHIZOME_SIZE_UNSET) { - if (config.debug.rhizome_manifest) - DEBUG("Missing 'filesize' field"); - ret = 0; - } else if (m->filesize == 0 && m->has_filehash) { - if (config.debug.rhizome_manifest) - DEBUG("Spurious 'filehash' field"); - ret = 0; - } else if (m->filesize != 0 && !m->has_filehash) { - if (config.debug.rhizome_manifest) - DEBUG("Missing 'filehash' field"); - ret = 0; - } - // Warn if expected fields are missing or invalid - if (m->service == NULL) { - if (config.debug.rhizome_manifest) - DEBUG("Missing 'service' field"); - ++m->malformed; - } + return rhizome_manifest_validate_reason(m) == NULL ? 1 : 0; +} + +/* If all essential (transport) fields are present and well formed then sets the m->finalised field + * and returns NULL, otherwise returns a pointer to a static string (not malloc(3)ed) describing the + * problem. + * + * If any non-essential fields are missing or invalid, then sets m->malformed to point to a static + * string describing the problem. It is up to the caller to check m->malformed and decide whether + * or not to process a malformed manifest. + * + * @author Andrew Bettison + */ +const char *rhizome_manifest_validate_reason(rhizome_manifest *m) +{ + const char *reason = NULL; + if (!m->has_id) + reason = "Missing 'id' field"; + else if (m->version == 0) + reason = "Missing 'version' field"; + else if (m->filesize == RHIZOME_SIZE_UNSET) + reason = "Missing 'filesize' field"; + else if (m->filesize == 0 && m->has_filehash) + reason = "Spurious 'filehash' field"; + else if (m->filesize != 0 && !m->has_filehash) + reason = "Missing 'filehash' field"; + if (reason && config.debug.rhizome_manifest) + DEBUG(reason); + if (m->service == NULL) + m->malformed = "Missing 'service' field"; else if (strcmp(m->service, RHIZOME_SERVICE_FILE) == 0) { - if (m->name == NULL) { - if (config.debug.rhizome_manifest) - DEBUG("Manifest with service='" RHIZOME_SERVICE_FILE "' missing 'name' field"); - ++m->malformed; - } + if (m->name == NULL) + m->malformed = "Manifest with service='" RHIZOME_SERVICE_FILE "' missing 'name' field"; } else if (strcmp(m->service, RHIZOME_SERVICE_MESHMS) == 0 || strcmp(m->service, RHIZOME_SERVICE_MESHMS2) == 0 ) { - if (!m->has_sender) { - if (config.debug.rhizome_manifest) - DEBUGF("Manifest with service='%s' missing 'sender' field", m->service); - ++m->malformed; - } - if (!m->has_recipient) { - if (config.debug.rhizome_manifest) - DEBUGF("Manifest with service='%s' missing 'recipient' field", m->service); - ++m->malformed; - } + if (!m->has_recipient) + m->malformed = "Manifest missing 'recipient' field"; + else if (!m->has_sender) + m->malformed = "Manifest missing 'sender' field"; } - else if (!rhizome_str_is_manifest_service(m->service)) { - if (config.debug.rhizome_manifest) - DEBUGF("Manifest invalid 'service' field %s", alloca_str_toprint(m->service)); - ++m->malformed; - } - if (!m->has_date) { - if (config.debug.rhizome_manifest) - DEBUG("Missing 'date' field"); - ++m->malformed; - } - m->finalised = ret; - return ret; + else if (!rhizome_str_is_manifest_service(m->service)) + m->malformed = "Manifest invalid 'service' field"; + else if (!m->has_date) + m->malformed = "Missing 'date' field"; + if (m->malformed && config.debug.rhizome_manifest) + DEBUG(m->malformed); + m->finalised = (reason == NULL); + return reason; } int rhizome_read_manifest_from_file(rhizome_manifest *m, const char *filename) From db8ee79a4ee11bc0ddf48e9c451fa813b9844851 Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Fri, 27 Jun 2014 17:29:57 +0930 Subject: [PATCH 08/21] Rhizome Java API: list bundles --- .../servalproject/json/JSONTableScanner.java | 113 ++++++++++++++ .../org/servalproject/json/JSONTokeniser.java | 14 ++ .../servaldna/ServalDClient.java | 9 ++ .../servaldna/rhizome/RhizomeBundle.java | 78 ++++++++++ .../servaldna/rhizome/RhizomeBundleList.java | 137 +++++++++++++++++ .../servaldna/rhizome/RhizomeCommon.java | 81 ++++++++++ java/org/servalproject/test/Rhizome.java | 81 ++++++++++ tests/all | 3 +- tests/rhizomejava | 141 ++++++++++++++++++ 9 files changed, 656 insertions(+), 1 deletion(-) create mode 100644 java/org/servalproject/json/JSONTableScanner.java create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeBundle.java create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeBundleList.java create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeCommon.java create mode 100644 java/org/servalproject/test/Rhizome.java create mode 100755 tests/rhizomejava diff --git a/java/org/servalproject/json/JSONTableScanner.java b/java/org/servalproject/json/JSONTableScanner.java new file mode 100644 index 00000000..3442b5e0 --- /dev/null +++ b/java/org/servalproject/json/JSONTableScanner.java @@ -0,0 +1,113 @@ +/** + * 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.json; + +import java.lang.reflect.InvocationTargetException; +import java.io.IOException; +import java.util.Vector; +import java.util.Map; +import java.util.HashMap; +import java.util.HashSet; + +public class JSONTableScanner { + + private static class Column { + public String label; + public Class type; + public JSONTokeniser.Narrow opts; + } + + HashMap columnMap; + Column[] columns; + + public JSONTableScanner() + { + columnMap = new HashMap(); + } + + public JSONTableScanner addColumn(String label, Class type) + { + return addColumn(label, type, JSONTokeniser.Narrow.NO_NULL); + } + + public JSONTableScanner addColumn(String label, Class type, JSONTokeniser.Narrow opts) + { + assert !columnMap.containsKey(label); + Column col = new Column(); + col.label = label; + col.type = type; + col.opts = opts; + columnMap.put(label, col); + return this; + } + + public void consumeHeaderArray(JSONTokeniser json) throws IOException, JSONInputException + { + Vector headers = new Vector(); + json.consumeArray(headers, String.class); + if (headers.size() < 1) + throw new JSONInputException("malformed JSON table, empty headers array"); + columns = new Column[headers.size()]; + HashSet headerSet = new HashSet(columnMap.size()); + for (int i = 0; i < headers.size(); ++i) { + String header = headers.get(i); + if (columnMap.containsKey(header)) { + if (headerSet.contains(header)) + throw new JSONInputException("malformed JSON table, duplicate column header: \"" + header + "\""); + headerSet.add(header); + columns[i] = columnMap.get(header); + } + } + for (String header: columnMap.keySet()) + if (!headerSet.contains(header)) + throw new JSONInputException("malformed JSON table, missing column header: \"" + header + "\""); + } + + @SuppressWarnings("unchecked") + public Map consumeRowArray(JSONTokeniser json) throws IOException, JSONInputException + { + Object[] row = new Object[columns.length]; + json.consumeArray(row, JSONTokeniser.Narrow.ALLOW_NULL); + HashMap rowMap = new HashMap(row.length); + for (int i = 0; i < row.length; ++i) { + Column col = columns[i]; + if (col != null) { + Object value; + if (JSONTokeniser.supportsNarrowTo(col.type)) + value = JSONTokeniser.narrow(row[i], col.type, col.opts); + else { + value = JSONTokeniser.narrow(row[i], col.opts); + try { + value = value == null ? null : col.type.getConstructor(value.getClass()).newInstance(value); + } + catch (InvocationTargetException e) { + throw new JSONInputException("invalid column value: " + col.label + "=\"" + value + "\"", e.getTargetException()); + } + catch (Exception e) { + throw new JSONInputException("invalid column value: " + col.label + "=\"" + value + "\"", e); + } + } + rowMap.put(col.label, value); + } + } + return rowMap; + } +} diff --git a/java/org/servalproject/json/JSONTokeniser.java b/java/org/servalproject/json/JSONTokeniser.java index 681a1ccf..9ea14c7a 100644 --- a/java/org/servalproject/json/JSONTokeniser.java +++ b/java/org/servalproject/json/JSONTokeniser.java @@ -131,6 +131,20 @@ public class JSONTokeniser { ALLOW_NULL }; + public static boolean supportsNarrowTo(Class cls) { + return cls == Boolean.class + || cls == Integer.class + || cls == Long.class + || cls == Float.class + || cls == Double.class + || cls == String.class; + } + + public static Object narrow(Object tok, Narrow opts) throws UnexpectedException + { + return narrow(tok, Object.class, opts); + } + public static T narrow(Object tok, Class cls) throws UnexpectedException { return narrow(tok, cls, Narrow.NO_NULL); diff --git a/java/org/servalproject/servaldna/ServalDClient.java b/java/org/servalproject/servaldna/ServalDClient.java index 27f74171..1b3562ad 100644 --- a/java/org/servalproject/servaldna/ServalDClient.java +++ b/java/org/servalproject/servaldna/ServalDClient.java @@ -34,6 +34,8 @@ import org.servalproject.codec.Base64; import org.servalproject.servaldna.SubscriberId; 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.meshms.MeshMSCommon; import org.servalproject.servaldna.meshms.MeshMSConversationList; import org.servalproject.servaldna.meshms.MeshMSMessageList; @@ -58,6 +60,13 @@ public class ServalDClient implements ServalDHttpConnectionFactory this.restfulPassword = restfulPassword; } + public RhizomeBundleList rhizomeListBundles() throws ServalDInterfaceException, IOException + { + RhizomeBundleList list = new RhizomeBundleList(this); + list.connect(); + return list; + } + 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 new file mode 100644 index 00000000..50acadb6 --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeBundle.java @@ -0,0 +1,78 @@ +/** + * 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 new file mode 100644 index 00000000..9ef446d5 --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeBundleList.java @@ -0,0 +1,137 @@ +/** + * 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.json.JSONInputException; +import org.servalproject.json.JSONTokeniser; +import org.servalproject.json.JSONTableScanner; +import org.servalproject.servaldna.ServalDHttpConnectionFactory; +import org.servalproject.servaldna.ServalDInterfaceException; +import org.servalproject.servaldna.BundleId; +import org.servalproject.servaldna.SubscriberId; +import org.servalproject.servaldna.FileHash; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class RhizomeBundleList { + + private ServalDHttpConnectionFactory httpConnector; + private HttpURLConnection httpConnection; + private JSONTokeniser json; + private JSONTableScanner table; + int rowCount; + + public RhizomeBundleList(ServalDHttpConnectionFactory connector) + { + this.httpConnector = connector; + this.table = new JSONTableScanner() + .addColumn("_id", Integer.class) + .addColumn(".token", String.class, JSONTokeniser.Narrow.ALLOW_NULL) + .addColumn("service", String.class) + .addColumn("id", BundleId.class) + .addColumn("version", Long.class) + .addColumn("date", Long.class) + .addColumn(".inserttime", Long.class) + .addColumn(".author", SubscriberId.class, JSONTokeniser.Narrow.ALLOW_NULL) + .addColumn(".fromhere", Integer.class) + .addColumn("filesize", Long.class) + .addColumn("filehash", FileHash.class, JSONTokeniser.Narrow.ALLOW_NULL) + .addColumn("sender", SubscriberId.class, JSONTokeniser.Narrow.ALLOW_NULL) + .addColumn("recipient", SubscriberId.class, JSONTokeniser.Narrow.ALLOW_NULL) + .addColumn("name", String.class); + } + + public boolean isConnected() + { + return this.json != null; + } + + public void connect() throws IOException, ServalDInterfaceException + { + try { + rowCount = 0; + httpConnection = httpConnector.newServalDHttpConnection("/restful/rhizome/bundlelist.json"); + httpConnection.connect(); + json = RhizomeCommon.receiveRestfulResponse(httpConnection, HttpURLConnection.HTTP_OK); + 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 RhizomeBundle nextBundle() 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 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")); + } + 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/org/servalproject/servaldna/rhizome/RhizomeCommon.java b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java new file mode 100644 index 00000000..7f881e02 --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java @@ -0,0 +1,81 @@ +/** + * 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.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import org.servalproject.servaldna.ServalDInterfaceException; +import org.servalproject.json.JSONTokeniser; +import org.servalproject.json.JSONInputException; + +public class RhizomeCommon +{ + protected static JSONTokeniser 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 JSONTokeniser receiveRestfulResponse(HttpURLConnection conn, int[] expected_response_codes) throws IOException, ServalDInterfaceException + { + 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()); + } + + private static class 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.END_OBJECT); + json.consume(JSONTokeniser.Token.EOF); + return status; + } + catch (JSONInputException e) { + throw new ServalDInterfaceException("malformed JSON status response", e); + } + } + +} diff --git a/java/org/servalproject/test/Rhizome.java b/java/org/servalproject/test/Rhizome.java new file mode 100644 index 00000000..c54fabca --- /dev/null +++ b/java/org/servalproject/test/Rhizome.java @@ -0,0 +1,81 @@ +/** + * 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.test; + +import java.io.IOException; +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.rhizome.RhizomeBundleList; + +public class Rhizome { + + static void rhizome_list() throws ServalDInterfaceException, IOException, InterruptedException + { + ServalDClient client = new ServerControl().getRestfulClient(); + RhizomeBundleList list = null; + try { + list = client.rhizomeListBundles(); + RhizomeBundle 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 + ); + } + } + 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("rhizome-list")) + rhizome_list(); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + System.err.println("No such command: " + methodName); + System.exit(1); + } +} diff --git a/tests/all b/tests/all index f7e7732d..f3ded7cc 100755 --- a/tests/all +++ b/tests/all @@ -2,7 +2,7 @@ # Aggregation of all tests except high-load stress tests. # -# Copyright 2012 Serval Project, Inc. +# Copyright 2012-2014 Serval Project Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -39,6 +39,7 @@ includeTests directory_service includeTests vomp if type -p "$JAVAC" >/dev/null; then includeTests jni + includeTests rhizomejava includeTests meshmsjava fi diff --git a/tests/rhizomejava b/tests/rhizomejava new file mode 100755 index 00000000..54fa7abf --- /dev/null +++ b/tests/rhizomejava @@ -0,0 +1,141 @@ +#!/bin/bash + +# Tests for Rhizome Java API. +# +# Copyright 2014 Serval Project Inc. +# +# 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_java.sh" +source "${0%/*}/../testdefs_rhizome.sh" + +setup() { + setup_servald + setup_servald_so + compile_java_classes + set_instance +A + executeOk_servald config \ + set log.console.level debug \ + set debug.httpd on + create_identities 4 + start_servald_server +} + +teardown() { + stop_all_servald_servers + kill_all_servald_processes + assert_no_servald_processes + report_all_servald_servers +} + +# Utility function: +# +# unset_vars_with_prefix PREFIX +# +# Unsets all shell variables whose names starting with the given PREFIX +unset_vars_with_prefix() { + local __prefix="${1?}" + local __varname + for __varname in $(declare -p | sed -n -e "s/^declare -[^ ]* \($__prefix[A-Za-z0-9_]\+\)=.*/\1/p"); do + unset $__varname + done +} + +# Utility function: +# +# unpack_vars PREFIX TEXT +# +# parses the given TEXT which must have the form: +# +# ident1=..., ident2=...., ... identN=... +# +# into shell variables: +# +# PREFIXident1=... +# PREFIXident2=... +# ... +# PREFIXidentN=... +# +# Sets the UNPACKED_VAR_NAMES[] array variable to a list of the names of the +# variables that were set (names include the PREFIX). +# +# Warning: overwrites existing shell variables. Names of overwritten shell +# variables are derived directly from the output of the command, so cannot be +# controlled. PREFIX should be used to ensure that special variables cannot +# be clobbered by accident. +unpack_vars() { + local __prefix="${1?}" + local __text="${2?}" + local __oo + tfw_shopt __oo -s extglob + UNPACKED_VAR_NAMES=() + while [ -n "$__text" ]; do + case "$__text" in + [A-Za-z_.]+([A-Za-z_.0-9])=*) + local __ident="${__text%%=*}" + __ident="${__ident//./__}" + __text="${__text#*=}" + local __value="${__text%%, [A-Za-z_.]+([A-Za-z_.0-9])=*}" + __text="${__text:${#__value}}" + __text="${__text#, }" + UNPACKED_VAR_NAMES+=("$__ident") + eval ${__prefix}${__ident}=\"\$__value\" + ;; + *) + fail "cannot unpack variable from '$__text'" + ;; + esac + done + tfw_shopt_restore __oo +} + +doc_RhizomeList="Java API Rhizome list 100 bundles" +setup_RhizomeList() { + setup + NBUNDLES=100 + rhizome_add_bundles $SIDA1 0 $((NBUNDLES-1)) + assert [ "$ROWID_MAX" -ge "$NBUNDLES" ] +} +test_RhizomeList() { + executeJavaOk org.servalproject.test.Rhizome rhizome-list + tfw_cat --stdout --stderr + assertStdoutLineCount == $NBUNDLES + 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" + if [ "${ROWID[$n]}" -eq "$ROWID_MAX" ]; then + # The first row must contain a non-null token string. + assert [ -n "$X____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" ] + let --lnum + done +} + +runTests "$@" From 93e67ede635930e678ac2714b0436de733a53c3d Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Tue, 1 Jul 2014 21:52:12 +0930 Subject: [PATCH 09/21] 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 "$@" From 34b6ff48bf9c44808867c3d37e2c4fc9677dcdd7 Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Tue, 1 Jul 2014 21:53:03 +0930 Subject: [PATCH 10/21] Rhizome Java API: get raw payload Added more "Serval-Rhizome-Bundle-xxx" headers to /restful/rhizome/ responses, so that a more complete manifest can be constructed from them --- .../servaldna/ServalDClient.java | 6 + .../servaldna/rhizome/RhizomeCommon.java | 120 ++++++++++++++++-- .../rhizome/RhizomePayloadRawBundle.java | 50 ++++++++ java/org/servalproject/test/Rhizome.java | 31 +++++ rhizome_restful.c | 16 +++ testdefs_rhizome.sh | 2 +- tests/rhizomejava | 17 ++- 7 files changed, 226 insertions(+), 16 deletions(-) create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomePayloadRawBundle.java diff --git a/java/org/servalproject/servaldna/ServalDClient.java b/java/org/servalproject/servaldna/ServalDClient.java index 1788d2fb..780e1f68 100644 --- a/java/org/servalproject/servaldna/ServalDClient.java +++ b/java/org/servalproject/servaldna/ServalDClient.java @@ -38,6 +38,7 @@ 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.rhizome.RhizomePayloadRawBundle; import org.servalproject.servaldna.meshms.MeshMSCommon; import org.servalproject.servaldna.meshms.MeshMSConversationList; import org.servalproject.servaldna.meshms.MeshMSMessageList; @@ -74,6 +75,11 @@ public class ServalDClient implements ServalDHttpConnectionFactory return RhizomeCommon.rhizomeManifest(this, bid); } + public RhizomePayloadRawBundle rhizomePayloadRaw(BundleId bid) throws ServalDInterfaceException, IOException + { + return RhizomeCommon.rhizomePayloadRaw(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/RhizomeCommon.java b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java index 4c7b3cd9..30eb9279 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java @@ -20,16 +20,20 @@ package org.servalproject.servaldna.rhizome; +import java.lang.StringBuilder; import java.lang.reflect.InvocationTargetException; import java.util.Map; import java.util.List; import java.io.IOException; +import java.io.PrintStream; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import org.servalproject.json.JSONTokeniser; import org.servalproject.json.JSONInputException; import org.servalproject.servaldna.BundleId; +import org.servalproject.servaldna.FileHash; +import org.servalproject.servaldna.BundleKey; import org.servalproject.servaldna.SubscriberId; import org.servalproject.servaldna.BundleSecret; import org.servalproject.servaldna.ServalDHttpConnectionFactory; @@ -117,18 +121,52 @@ public class RhizomeCommon 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); - } - } + dumpHeaders(conn, System.err); 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); } + public static RhizomePayloadRawBundle rhizomePayloadRaw(ServalDHttpConnectionFactory connector, BundleId bid) throws IOException, ServalDInterfaceException + { + HttpURLConnection conn = connector.newServalDHttpConnection("/restful/rhizome/" + bid.toHex() + "/raw.bin"); + conn.connect(); + InputStream in = RhizomeCommon.receiveResponse(conn, HttpURLConnection.HTTP_OK); + if (!conn.getContentType().equals("application/octet-stream")) + throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType()); + dumpHeaders(conn, System.err); + RhizomeManifest manifest = manifestFromHeaders(conn); + 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 RhizomePayloadRawBundle(manifest, in, insertTime, author, secret); + } + + private static void dumpHeaders(HttpURLConnection conn, PrintStream out) + { + for (Map.Entry> e: conn.getHeaderFields().entrySet()) + for (String v: e.getValue()) + out.println("received header " + e.getKey() + ": " + v); + } + + private static RhizomeManifest manifestFromHeaders(HttpURLConnection conn) throws ServalDInterfaceException + { + BundleId id = header(conn, "Serval-Rhizome-Bundle-Id", BundleId.class); + long version = headerUnsignedLong(conn, "Serval-Rhizome-Bundle-Version"); + long filesize = headerUnsignedLong(conn, "Serval-Rhizome-Bundle-Filesize"); + FileHash filehash = filesize == 0 ? null : header(conn, "Serval-Rhizome-Bundle-Filehash", FileHash.class); + SubscriberId sender = headerOrNull(conn, "Serval-Rhizome-Bundle-Sender", SubscriberId.class); + SubscriberId recipient = headerOrNull(conn, "Serval-Rhizome-Bundle-Recipient", SubscriberId.class); + BundleKey BK = headerOrNull(conn, "Serval-Rhizome-Bundle-BK", BundleKey.class); + Integer crypt = headerIntegerOrNull(conn, "Serval-Rhizome-Bundle-Crypt"); + Long tail = headerUnsignedLongOrNull(conn, "Serval-Rhizome-Bundle-Tail"); + Long date = headerUnsignedLongOrNull(conn, "Serval-Rhizome-Bundle-Date"); + String service = conn.getHeaderField("Serval-Rhizome-Bundle-Service"); + String name = headerQuotedStringOrNull(conn, "Serval-Rhizome-Bundle-Name"); + return new RhizomeManifest(id, version, filesize, filehash, sender, recipient, BK, crypt, tail, date, service, name); + } + private static String headerString(HttpURLConnection conn, String header) throws ServalDInterfaceException { String str = conn.getHeaderField(header); @@ -137,22 +175,58 @@ public class RhizomeCommon return str; } - private static int headerInteger(HttpURLConnection conn, String header) throws ServalDInterfaceException + private static String headerQuotedStringOrNull(HttpURLConnection conn, String header) throws ServalDInterfaceException { - String str = headerString(conn, header); + String quoted = conn.getHeaderField(header); + if (quoted == null) + return null; + if (quoted.length() == 0 || quoted.charAt(0) != '"') + throw new ServalDInterfaceException("malformed header field: " + header + ": missing quote at start of quoted-string"); + boolean slosh = false; + boolean end = false; + StringBuilder b = new StringBuilder(quoted.length()); + for (int i = 1; i < quoted.length(); ++i) { + char c = quoted.charAt(i); + if (end) + throw new ServalDInterfaceException("malformed header field: " + header + ": spurious character after quoted-string"); + if (c < ' ' || c > '~') + throw new ServalDInterfaceException("malformed header field: " + header + ": invalid character in quoted-string"); + if (slosh) { + b.append(c); + slosh = false; + } + else if (c == '"') + end = true; + else if (c == '\\') + slosh = true; + else + b.append(c); + } + if (!end) + throw new ServalDInterfaceException("malformed header field: " + header + ": missing quote at end of quoted-string"); + return b.toString(); + } + + private static Integer headerIntegerOrNull(HttpURLConnection conn, String header) throws ServalDInterfaceException + { + String str = conn.getHeaderField(header); + if (str == null) + return null; try { - return Integer.parseInt(str); + return Integer.valueOf(str); } catch (NumberFormatException e) { } throw new ServalDInterfaceException("invalid header field: " + header + ": " + str); } - private static long headerUnsignedLong(HttpURLConnection conn, String header) throws ServalDInterfaceException + private static Long headerUnsignedLongOrNull(HttpURLConnection conn, String header) throws ServalDInterfaceException { - String str = headerString(conn, header); + String str = conn.getHeaderField(header); + if (str == null) + return null; try { - long value = Long.parseLong(str); + Long value = Long.valueOf(str); if (value >= 0) return value; } @@ -161,9 +235,19 @@ public class RhizomeCommon throw new ServalDInterfaceException("invalid header field: " + header + ": " + str); } - private static T header(HttpURLConnection conn, String header, Class cls) throws ServalDInterfaceException + private static long headerUnsignedLong(HttpURLConnection conn, String header) throws ServalDInterfaceException { - String str = headerString(conn, header); + Long value = headerUnsignedLongOrNull(conn, header); + if (value == null) + throw new ServalDInterfaceException("missing header field: " + header); + return value; + } + + private static T headerOrNull(HttpURLConnection conn, String header, Class cls) throws ServalDInterfaceException + { + String str = conn.getHeaderField(header); + if (str == null) + return null; try { return (T) cls.getConstructor(String.class).newInstance(str); } @@ -175,4 +259,12 @@ public class RhizomeCommon } } + private static T header(HttpURLConnection conn, String header, Class cls) throws ServalDInterfaceException + { + T value = headerOrNull(conn, header, cls); + if (value == null) + throw new ServalDInterfaceException("missing header field: " + header); + return value; + } + } diff --git a/java/org/servalproject/servaldna/rhizome/RhizomePayloadRawBundle.java b/java/org/servalproject/servaldna/rhizome/RhizomePayloadRawBundle.java new file mode 100644 index 00000000..ca00792d --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomePayloadRawBundle.java @@ -0,0 +1,50 @@ +/** + * 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.InputStream; +import org.servalproject.servaldna.SubscriberId; +import org.servalproject.servaldna.BundleSecret; +import org.servalproject.servaldna.ServalDInterfaceException; + +public class RhizomePayloadRawBundle { + + public final RhizomeManifest manifest; + public final InputStream rawPayloadInputStream; + public final long insertTime; + public final SubscriberId author; + public final BundleSecret secret; + + protected RhizomePayloadRawBundle(RhizomeManifest manifest, + InputStream rawPayloadInputStream, + long insertTime, + SubscriberId author, + BundleSecret secret) + + { + this.rawPayloadInputStream = rawPayloadInputStream; + this.manifest = manifest; + this.insertTime = insertTime; + this.author = author; + this.secret = secret; + } + +} diff --git a/java/org/servalproject/test/Rhizome.java b/java/org/servalproject/test/Rhizome.java index 5538c13a..0e572191 100644 --- a/java/org/servalproject/test/Rhizome.java +++ b/java/org/servalproject/test/Rhizome.java @@ -32,6 +32,7 @@ import org.servalproject.servaldna.rhizome.RhizomeManifest; import org.servalproject.servaldna.rhizome.RhizomeListBundle; import org.servalproject.servaldna.rhizome.RhizomeBundleList; import org.servalproject.servaldna.rhizome.RhizomeManifestBundle; +import org.servalproject.servaldna.rhizome.RhizomePayloadRawBundle; public class Rhizome { @@ -90,6 +91,34 @@ public class Rhizome { System.exit(0); } + static void rhizome_payload_raw(BundleId bid, String dstpath) throws ServalDInterfaceException, IOException, InterruptedException + { + ServalDClient client = new ServerControl().getRestfulClient(); + FileOutputStream out = new FileOutputStream(dstpath); + try { + RhizomePayloadRawBundle bundle = client.rhizomePayloadRaw(bid); + InputStream in = bundle.rawPayloadInputStream; + byte[] buf = new byte[4096]; + int n; + while ((n = in.read(buf)) > 0) + out.write(buf, 0, n); + in.close(); + out.close(); + out = null; + System.out.println( + "_insertTime=" + bundle.insertTime + "\n" + + "_author=" + bundle.author + "\n" + + "_secret=" + bundle.secret + "\n" + + manifestFields(bundle.manifest, "\n") + "\n" + ); + } + finally { + if (out != null) + out.close(); + } + System.exit(0); + } + public static void main(String... args) { if (args.length < 1) @@ -100,6 +129,8 @@ public class Rhizome { rhizome_list(); else if (methodName.equals("rhizome-manifest")) rhizome_manifest(new BundleId(args[1]), args[2]); + else if (methodName.equals("rhizome-payload-raw")) + rhizome_payload_raw(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 4c5647b8..f55a9639 100644 --- a/rhizome_restful.c +++ b/rhizome_restful.c @@ -764,8 +764,24 @@ static void render_manifest_headers(struct http_request *hr, strbuf sb) strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Filesize: %"PRIu64"\r\n", m->filesize); if (m->filesize != 0) strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Filehash: %s\r\n", alloca_tohex_rhizome_filehash_t(m->filehash)); + if (m->has_sender) + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Sender: %s\r\n", alloca_tohex_sid_t(m->sender)); + if (m->has_recipient) + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Recipient: %s\r\n", alloca_tohex_sid_t(m->recipient)); if (m->has_bundle_key) strbuf_sprintf(sb, "Serval-Rhizome-Bundle-BK: %s\r\n", alloca_tohex_rhizome_bk_t(m->bundle_key)); + switch (m->payloadEncryption) { + case PAYLOAD_CRYPT_UNKNOWN: + break; + case PAYLOAD_CLEAR: + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Crypt: 0\r\n"); + break; + case PAYLOAD_ENCRYPTED: + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Crypt: 1\r\n"); + break; + } + if (m->is_journal) + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Tail: %"PRIu64"\r\n", m->tail); if (m->has_date) strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Date: %"PRIu64"\r\n", m->date); if (m->name) { diff --git a/testdefs_rhizome.sh b/testdefs_rhizome.sh index 98a4ac91..c3e3ff38 100644 --- a/testdefs_rhizome.sh +++ b/testdefs_rhizome.sh @@ -525,7 +525,7 @@ rhizome_add_bundles() { if $encrypted; then echo "crypt=1" >file$n.manifest fi - executeOk_servald rhizome add file $SID file$n file$n.manifest + executeOk_servald rhizome add file "$SID" file$n file$n.manifest extract_stdout_manifestid BID[$n] extract_stdout_version VERSION[$n] extract_stdout_filesize SIZE[$n] diff --git a/tests/rhizomejava b/tests/rhizomejava index ea0494b5..0b199221 100755 --- a/tests/rhizomejava +++ b/tests/rhizomejava @@ -161,7 +161,7 @@ setup_RhizomeManifest() { } test_RhizomeManifest() { for n in 0 1 2; do - executeJavaOk org.servalproject.test.Rhizome rhizome-manifest ${BID[$n]} bundle$n.rhm + 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 @@ -169,4 +169,19 @@ test_RhizomeManifest() { done } +doc_RhizomePayloadRaw="Java API fetch Rhizome raw payload" +setup_RhizomePayloadRaw() { + setup + rhizome_add_bundles $SIDA1 0 1 + rhizome_add_bundles --encrypted $SIDA1 2 3 +} +test_RhizomePayloadRaw() { + for n in 0 1 2 3; do + executeJavaOk org.servalproject.test.Rhizome rhizome-payload-raw "${BID[$n]}" raw.bin$n + tfw_cat --stdout --stderr + assert_metadata $n + assert cmp raw$n raw.bin$n + done +} + runTests "$@" From cf4363578988cafa23301d8b0c1cb8a3db3c08b9 Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Wed, 2 Jul 2014 17:16:19 +0930 Subject: [PATCH 11/21] Improve Rhizome HTTP RESTful interface Add RHIZOME_BUNDLE_STATUS_READONLY enum value Tighten up switch statements on bundle and payload status enums (no default labels) Rename some recently added enum entries Return bundle status and payload status in HTTP responses Add test for failing to decrypt a foreign encrypted bundle payload, fix bug that caused an assertion failure Add tests for fetching a non-existent manifest and fetching bundles whose payload blob is not in the store --- commandline.c | 28 ++-- http_server.c | 44 +++--- http_server.h | 6 +- httpd.c | 2 + httpd.h | 3 +- meshms_restful.c | 47 +++--- rhizome.c | 83 +++++++---- rhizome.h | 15 +- rhizome_bundle.c | 11 +- rhizome_crypto.c | 1 - rhizome_database.c | 5 +- rhizome_direct_http.c | 14 +- rhizome_fetch.c | 10 +- rhizome_restful.c | 326 ++++++++++++++++++++++++++++-------------- rhizome_store.c | 22 +-- testdefs_rhizome.sh | 10 ++ tests/rhizomerestful | 222 +++++++++++++++++++++++++--- 17 files changed, 604 insertions(+), 245 deletions(-) diff --git a/commandline.c b/commandline.c index 24841c83..06d58955 100644 --- a/commandline.c +++ b/commandline.c @@ -1761,9 +1761,9 @@ int app_rhizome_add_file(const struct cli_parsed *parsed, struct cli_context *co case RHIZOME_PAYLOAD_STATUS_NEW: break; case RHIZOME_PAYLOAD_STATUS_TOO_BIG: - case RHIZOME_PAYLOAD_STATUS_UNINITERESTING: - status = RHIZOME_BUNDLE_STATUS_DONOTWANT; - WHY("Insufficient space to store payload"); + case RHIZOME_PAYLOAD_STATUS_EVICTED: + status = RHIZOME_BUNDLE_STATUS_NO_ROOM; + INFO("Insufficient space to store payload"); break; case RHIZOME_PAYLOAD_STATUS_ERROR: status = RHIZOME_BUNDLE_STATUS_ERROR; @@ -1773,12 +1773,12 @@ int app_rhizome_add_file(const struct cli_parsed *parsed, struct cli_context *co status = RHIZOME_BUNDLE_STATUS_INCONSISTENT; break; case RHIZOME_PAYLOAD_STATUS_CRYPTO_FAIL: - status = RHIZOME_BUNDLE_STATUS_FAKE; + status = RHIZOME_BUNDLE_STATUS_READONLY; break; default: FATALF("pstatus = %d", pstatus); } - rhizome_manifest *mout = m; + rhizome_manifest *mout = NULL; if (status == RHIZOME_BUNDLE_STATUS_NEW) { if (!rhizome_manifest_validate(m) || m->malformed) status = RHIZOME_BUNDLE_STATUS_INVALID; @@ -1790,6 +1790,7 @@ int app_rhizome_add_file(const struct cli_parsed *parsed, struct cli_context *co } } } + int status_valid = 0; switch (status) { case RHIZOME_BUNDLE_STATUS_NEW: if (mout && mout != m) @@ -1799,25 +1800,30 @@ int app_rhizome_add_file(const struct cli_parsed *parsed, struct cli_context *co case RHIZOME_BUNDLE_STATUS_SAME: case RHIZOME_BUNDLE_STATUS_DUPLICATE: case RHIZOME_BUNDLE_STATUS_OLD: + assert(mout != NULL); cli_put_manifest(context, mout); if ( manifestpath && *manifestpath && rhizome_write_manifest_file(mout, manifestpath, 0) == -1 ) WHYF("Could not write manifest to %s", alloca_str_toprint(manifestpath)); + status_valid = 1; break; + case RHIZOME_BUNDLE_STATUS_READONLY: case RHIZOME_BUNDLE_STATUS_INCONSISTENT: case RHIZOME_BUNDLE_STATUS_ERROR: case RHIZOME_BUNDLE_STATUS_INVALID: case RHIZOME_BUNDLE_STATUS_FAKE: - case RHIZOME_BUNDLE_STATUS_DONOTWANT: + case RHIZOME_BUNDLE_STATUS_NO_ROOM: + status_valid = 1; break; - default: - FATALF("status=%d", status); + // Do not use a default: label! With no default, if a new value is added to the enum, then the + // compiler will issue a warning on switch statements that do not cover all the values, which is + // a valuable tool for the developer. } - if (mout && mout != m) { + if (!status_valid) + FATALF("status=%d", status); + if (mout && mout != m) rhizome_manifest_free(mout); - m = NULL; - } rhizome_manifest_free(m); keyring_free(keyring); keyring = NULL; diff --git a/http_server.c b/http_server.c index 298b9ba8..28f0c921 100644 --- a/http_server.c +++ b/http_server.c @@ -1840,12 +1840,14 @@ static strbuf strbuf_status_body(strbuf sb, struct http_response *hr, const char ) { hr->header.content_type = CONTENT_TYPE_TEXT; strbuf_sprintf(sb, "%03u %s", hr->result_code, message); - if (hr->result_extra_label) { - strbuf_puts(sb, "\r\n"); - strbuf_puts(sb, hr->result_extra_label); - strbuf_puts(sb, "="); - strbuf_json_atom_as_text(sb, &hr->result_extra_value); - } + unsigned i; + for (i = 0; i < NELS(hr->result_extra); ++i) + if (hr->result_extra[i].label) { + strbuf_puts(sb, "\r\n"); + strbuf_puts(sb, hr->result_extra[i].label); + strbuf_puts(sb, "="); + strbuf_json_atom_as_text(sb, &hr->result_extra[i].value); + } strbuf_puts(sb, "\r\n"); } else if ( hr->header.content_type == CONTENT_TYPE_JSON @@ -1854,24 +1856,28 @@ static strbuf strbuf_status_body(strbuf sb, struct http_response *hr, const char hr->header.content_type = CONTENT_TYPE_JSON; strbuf_sprintf(sb, "{\n \"http_status_code\": %u,\n \"http_status_message\": ", hr->result_code); strbuf_json_string(sb, message); - if (hr->result_extra_label) { - strbuf_puts(sb, ",\n "); - strbuf_json_string(sb, hr->result_extra_label); - strbuf_puts(sb, ": "); - strbuf_json_atom_as_html(sb, &hr->result_extra_value); - } + unsigned i; + for (i = 0; i < NELS(hr->result_extra); ++i) + if (hr->result_extra[i].label) { + strbuf_puts(sb, ",\n "); + strbuf_json_string(sb, hr->result_extra[i].label); + strbuf_puts(sb, ": "); + strbuf_json_atom(sb, &hr->result_extra[i].value); + } strbuf_puts(sb, "\n}"); } else { hr->header.content_type = CONTENT_TYPE_HTML; strbuf_sprintf(sb, "\n

%03u %s

", hr->result_code, message); - if (hr->result_extra_label) { - strbuf_puts(sb, "\n
"); - strbuf_html_escape(sb, hr->result_extra_label, strlen(hr->result_extra_label)); - strbuf_puts(sb, "
"); - strbuf_json_atom_as_html(sb, &hr->result_extra_value); - strbuf_puts(sb, "
"); - } + unsigned i; + for (i = 0; i < NELS(hr->result_extra); ++i) + if (hr->result_extra[i].label) { + strbuf_puts(sb, "\n
"); + strbuf_html_escape(sb, hr->result_extra[i].label, strlen(hr->result_extra[i].label)); + strbuf_puts(sb, "
"); + strbuf_json_atom_as_html(sb, &hr->result_extra[i].value); + strbuf_puts(sb, "
"); + } strbuf_puts(sb, "\n"); } return sb; diff --git a/http_server.h b/http_server.h index 6c4ed923..7de5bb9c 100644 --- a/http_server.h +++ b/http_server.h @@ -111,8 +111,10 @@ typedef int (HTTP_CONTENT_GENERATOR)(struct http_request *, unsigned char *, siz struct http_response { uint16_t result_code; - const char *result_extra_label; - struct json_atom result_extra_value; + struct { + const char *label; + struct json_atom value; + } result_extra[4]; struct http_response_headers header; const char *content; HTTP_CONTENT_GENERATOR *content_generator; // callback to produce more content diff --git a/httpd.c b/httpd.c index b462b0ae..4c6c9c4d 100644 --- a/httpd.c +++ b/httpd.c @@ -276,6 +276,8 @@ void httpd_server_poll(struct sched_ent *alarm) } else { ++httpd_request_count; request->uuid = http_request_uuid_counter++; + request->payload_status = INVALID_RHIZOME_PAYLOAD_STATUS; + request->bundle_status = INVALID_RHIZOME_BUNDLE_STATUS; if (peerip) request->http.client_sockaddr_in = *peerip; request->http.handle_headers = httpd_dispatch; diff --git a/httpd.h b/httpd.h index 76cca441..12dc8dbb 100644 --- a/httpd.h +++ b/httpd.h @@ -60,6 +60,8 @@ typedef struct httpd_request /* For requests/responses that pertain to a single manifest. */ rhizome_manifest *manifest; + enum rhizome_payload_status payload_status; + enum rhizome_bundle_status bundle_status; /* For requests/responses that contain one or two SIDs. */ @@ -123,7 +125,6 @@ typedef struct httpd_request // For storing the manifest text (malloc/realloc) as we receive it struct form_buf_malloc manifest; // For receiving the payload - enum rhizome_payload_status payload_status; uint64_t payload_size; struct rhizome_write write; } diff --git a/meshms_restful.c b/meshms_restful.c index ae3149f7..308cf01c 100644 --- a/meshms_restful.c +++ b/meshms_restful.c @@ -66,43 +66,40 @@ static int strn_to_meshms_token(const char *str, rhizome_bid_t *bidp, uint64_t * static int http_request_meshms_response(struct httpd_request *r, uint16_t result, const char *message, enum meshms_status status) { - r->http.response.result_extra_label = "meshms_status_code"; - r->http.response.result_extra_value.type = JSON_INTEGER; - r->http.response.result_extra_value.u.integer = status; + r->http.response.result_extra[0].label = "meshms_status_code"; + r->http.response.result_extra[0].value.type = JSON_INTEGER; + r->http.response.result_extra[0].value.u.integer = status; + uint16_t meshms_result = 0; + const char *meshms_message = NULL; switch (status) { case MESHMS_STATUS_OK: - if (!result) - result = 200; - if (!message) - message = "OK"; + meshms_result = 200; + meshms_message = "OK"; break; case MESHMS_STATUS_UPDATED: - if (!result) - result = 201; - if (!message) - message = "Updated"; + meshms_result = 201; + meshms_message = "Updated"; break; case MESHMS_STATUS_SID_LOCKED: - if (!result) - result = 403; - if (!message) - message = "Identity unknown"; + meshms_result = 403; + meshms_message = "Identity unknown"; break; case MESHMS_STATUS_PROTOCOL_FAULT: - if (!result) - result = 403; - if (!message) - message = "MeshMS protocol fault"; + meshms_result = 403; + meshms_message = "MeshMS protocol fault"; break; case MESHMS_STATUS_ERROR: - if (!result) - result = 500; - break; - default: - WHYF("Invalid MeshMS status code %d", status); - result = 500; + meshms_result = 500; break; } + if (!meshms_result) { + WHYF("Invalid MeshMS status code %d", status); + result = 500; + } else if (!result) { + result = meshms_result; + if (message == NULL) + message = meshms_message; + } assert(result != 0); http_request_simple_response(&r->http, result, message); return result; diff --git a/rhizome.c b/rhizome.c index 85105b4b..99820515 100644 --- a/rhizome.c +++ b/rhizome.c @@ -158,31 +158,27 @@ enum rhizome_bundle_status rhizome_bundle_import_files(rhizome_manifest *m, rhiz ) return RHIZOME_BUNDLE_STATUS_INVALID; enum rhizome_bundle_status status = rhizome_manifest_check_stored(m, mout); - if (status == RHIZOME_BUNDLE_STATUS_NEW) { - enum rhizome_payload_status pstatus = rhizome_import_payload_from_file(m, filepath); - switch (pstatus) { - case RHIZOME_PAYLOAD_STATUS_EMPTY: - case RHIZOME_PAYLOAD_STATUS_STORED: - case RHIZOME_PAYLOAD_STATUS_NEW: - if (rhizome_store_manifest(m) == -1) - return -1; - break; - case RHIZOME_PAYLOAD_STATUS_TOO_BIG: - case RHIZOME_PAYLOAD_STATUS_UNINITERESTING: - status = RHIZOME_BUNDLE_STATUS_DONOTWANT; - break; - case RHIZOME_PAYLOAD_STATUS_ERROR: - case RHIZOME_PAYLOAD_STATUS_CRYPTO_FAIL: + if (status != RHIZOME_BUNDLE_STATUS_NEW) + return status; + enum rhizome_payload_status pstatus = rhizome_import_payload_from_file(m, filepath); + switch (pstatus) { + case RHIZOME_PAYLOAD_STATUS_EMPTY: + case RHIZOME_PAYLOAD_STATUS_STORED: + case RHIZOME_PAYLOAD_STATUS_NEW: + if (rhizome_store_manifest(m) == -1) return -1; - case RHIZOME_PAYLOAD_STATUS_WRONG_SIZE: - case RHIZOME_PAYLOAD_STATUS_WRONG_HASH: - status = RHIZOME_BUNDLE_STATUS_INCONSISTENT; - break; - default: - FATALF("pstatus = %d", pstatus); - } + return status; + case RHIZOME_PAYLOAD_STATUS_TOO_BIG: + case RHIZOME_PAYLOAD_STATUS_EVICTED: + return RHIZOME_BUNDLE_STATUS_NO_ROOM; + case RHIZOME_PAYLOAD_STATUS_ERROR: + case RHIZOME_PAYLOAD_STATUS_CRYPTO_FAIL: + return -1; + case RHIZOME_PAYLOAD_STATUS_WRONG_SIZE: + case RHIZOME_PAYLOAD_STATUS_WRONG_HASH: + return RHIZOME_BUNDLE_STATUS_INCONSISTENT; } - return status; + FATALF("rhizome_import_payload_from_file() returned status = %d", pstatus); } /* Sets the bundle key "BK" field of a manifest. Returns 1 if the field was set, 0 if not. @@ -325,8 +321,12 @@ enum rhizome_bundle_status rhizome_manifest_check_stored(rhizome_manifest *m, rh enum rhizome_bundle_status rhizome_add_manifest(rhizome_manifest *m, rhizome_manifest **mout) { - if (config.debug.rhizome) - DEBUGF("rhizome_add_manifest(m=manifest[%d](%p), mout=%p)", m->manifest_record_number, m, mout); + if (config.debug.rhizome) { + if (mout == NULL) + DEBUGF("rhizome_add_manifest(m=manifest[%d](%p), mout=NULL)", m->manifest_record_number, m); + else + DEBUGF("rhizome_add_manifest(m=manifest[%d](%p), *mout=manifest[%d](%p))", m->manifest_record_number, m, *mout ? (*mout)->manifest_record_number : -1, *mout); + } if (!m->finalised && !rhizome_manifest_validate(m)) return RHIZOME_BUNDLE_STATUS_INVALID; assert(m->finalised); @@ -351,3 +351,36 @@ int rhizome_saw_voice_traffic() rhizome_voice_timeout=gettime_ms()+1000; return 0; } + +const char *rhizome_bundle_status_message(enum rhizome_bundle_status status) +{ + switch (status) { + case RHIZOME_BUNDLE_STATUS_NEW: return "Bundle new to store"; + case RHIZOME_BUNDLE_STATUS_SAME: return "Bundle already in store"; + case RHIZOME_BUNDLE_STATUS_DUPLICATE: return "Duplicate bundle already in store"; + case RHIZOME_BUNDLE_STATUS_OLD: return "Newer bundle already in store"; + case RHIZOME_BUNDLE_STATUS_INVALID: return "Invalid manifest"; + case RHIZOME_BUNDLE_STATUS_FAKE: return "Manifest signature does not verify"; + case RHIZOME_BUNDLE_STATUS_INCONSISTENT: return "Manifest inconsistent with supplied payload"; + case RHIZOME_BUNDLE_STATUS_NO_ROOM: return "No room in store for bundle"; + case RHIZOME_BUNDLE_STATUS_READONLY: return "Bundle is read-only"; + case RHIZOME_BUNDLE_STATUS_ERROR: return "Internal error"; + } + return NULL; +} + +const char *rhizome_payload_status_message(enum rhizome_payload_status status) +{ + switch (status) { + case RHIZOME_PAYLOAD_STATUS_NEW: return "Payload new to store"; + case RHIZOME_PAYLOAD_STATUS_STORED: return "Payload already in store"; + case RHIZOME_PAYLOAD_STATUS_EMPTY: return "Payload empty"; + case RHIZOME_PAYLOAD_STATUS_TOO_BIG: return "Payload size exceeds store"; + case RHIZOME_PAYLOAD_STATUS_EVICTED: return "Payload evicted"; + case RHIZOME_PAYLOAD_STATUS_WRONG_SIZE: return "Payload size contradicts manifest"; + case RHIZOME_PAYLOAD_STATUS_WRONG_HASH: return "Payload hash contradicts manifest"; + case RHIZOME_PAYLOAD_STATUS_CRYPTO_FAIL: return "Incorrect bundle secret"; + case RHIZOME_PAYLOAD_STATUS_ERROR: return "Internal error"; + } + return NULL; +} diff --git a/rhizome.h b/rhizome.h index 5c9a2b46..8edd417a 100644 --- a/rhizome.h +++ b/rhizome.h @@ -362,21 +362,30 @@ enum rhizome_bundle_status { RHIZOME_BUNDLE_STATUS_INVALID = 4, // manifest is invalid RHIZOME_BUNDLE_STATUS_FAKE = 5, // manifest signature not valid RHIZOME_BUNDLE_STATUS_INCONSISTENT = 6, // manifest filesize/filehash does not match supplied payload - RHIZOME_BUNDLE_STATUS_DONOTWANT = 7, // Wont fit or we already have more important bundles + RHIZOME_BUNDLE_STATUS_NO_ROOM = 7, // doesn't fit; store may contain more important bundles + RHIZOME_BUNDLE_STATUS_READONLY = 8, // cannot modify manifest; secret unknown }; +#define INVALID_RHIZOME_BUNDLE_STATUS ((enum rhizome_bundle_status)-2) + +const char *rhizome_bundle_status_message(enum rhizome_bundle_status); + enum rhizome_payload_status { RHIZOME_PAYLOAD_STATUS_ERROR = -1, RHIZOME_PAYLOAD_STATUS_EMPTY = 0, // payload is empty (zero length) - RHIZOME_PAYLOAD_STATUS_NEW = 1, // payload is not yet in store + RHIZOME_PAYLOAD_STATUS_NEW = 1, // payload is not yet in store (added) RHIZOME_PAYLOAD_STATUS_STORED = 2, // payload is already in store RHIZOME_PAYLOAD_STATUS_WRONG_SIZE = 3, // payload's size does not match manifest RHIZOME_PAYLOAD_STATUS_WRONG_HASH = 4, // payload's hash does not match manifest RHIZOME_PAYLOAD_STATUS_CRYPTO_FAIL = 5, // cannot encrypt/decrypt (payload key unknown) RHIZOME_PAYLOAD_STATUS_TOO_BIG = 6, // payload will never fit in our store - RHIZOME_PAYLOAD_STATUS_UNINITERESTING = 7, // other payloads in our store are more interesting + RHIZOME_PAYLOAD_STATUS_EVICTED = 7, // other payloads in our store are more important }; +#define INVALID_RHIZOME_PAYLOAD_STATUS ((enum rhizome_bundle_status)-2) + +const char *rhizome_payload_status_message(enum rhizome_payload_status); + int rhizome_write_manifest_file(rhizome_manifest *m, const char *filename, char append); int rhizome_manifest_selfsign(rhizome_manifest *m); int rhizome_read_manifest_from_file(rhizome_manifest *m, const char *filename); diff --git a/rhizome_bundle.c b/rhizome_bundle.c index 7822ccbc..b4c742ba 100644 --- a/rhizome_bundle.c +++ b/rhizome_bundle.c @@ -1086,17 +1086,23 @@ int rhizome_manifest_dump(rhizome_manifest *m, const char *msg) enum rhizome_bundle_status rhizome_manifest_finalise(rhizome_manifest *m, rhizome_manifest **mout, int deduplicate) { IN(); - + assert(*mout == NULL); if (!m->finalised && !rhizome_manifest_validate(m)) RETURN(RHIZOME_BUNDLE_STATUS_INVALID); - // if a manifest was supplied with an ID, don't bother to check for a duplicate. // we only want to filter out added files with no existing manifest. if (deduplicate && m->haveSecret != EXISTING_BUNDLE_ID) { enum rhizome_bundle_status status = rhizome_find_duplicate(m, mout); switch (status) { case RHIZOME_BUNDLE_STATUS_DUPLICATE: + assert(*mout != NULL); + assert(*mout != m); + RETURN(status); case RHIZOME_BUNDLE_STATUS_ERROR: + if (*mout != NULL && *mout != m) { + rhizome_manifest_free(*mout); + *mout = NULL; + } RETURN(status); case RHIZOME_BUNDLE_STATUS_NEW: break; @@ -1104,6 +1110,7 @@ enum rhizome_bundle_status rhizome_manifest_finalise(rhizome_manifest *m, rhizom FATALF("rhizome_find_duplicate() returned %d", status); } } + assert(*mout == NULL); *mout = m; /* Convert to final form for signing and writing to disk */ diff --git a/rhizome_crypto.c b/rhizome_crypto.c index efb2f640..ae62efc2 100644 --- a/rhizome_crypto.c +++ b/rhizome_crypto.c @@ -600,7 +600,6 @@ int rhizome_crypt_xor_block(unsigned char *buffer, size_t buffer_size, uint64_t */ int rhizome_derive_payload_key(rhizome_manifest *m) { - // don't do anything if the manifest isn't flagged as being encrypted assert(m->payloadEncryption == PAYLOAD_ENCRYPTED); unsigned char hash[crypto_hash_sha512_BYTES]; if (m->has_sender && m->has_recipient) { diff --git a/rhizome_database.c b/rhizome_database.c index 3b97a52e..6b818317 100644 --- a/rhizome_database.c +++ b/rhizome_database.c @@ -1272,10 +1272,7 @@ int rhizome_cleanup(struct rhizome_cleanup_report *report) int rhizome_store_manifest(rhizome_manifest *m) { assert(m->finalised); - - // If we don't have the secret for this manifest, only store it if its self-signature is valid - if (!m->haveSecret && !m->selfSigned) - return WHY("Manifest is not signed, and I don't have the key. Manifest might be forged or corrupt."); + assert(m->haveSecret || m->selfSigned); // should not store an invalid or fake manifest /* Bind BAR to data field */ rhizome_bar_t bar; diff --git a/rhizome_direct_http.c b/rhizome_direct_http.c index 56316d1d..43791738 100644 --- a/rhizome_direct_http.c +++ b/rhizome_direct_http.c @@ -119,9 +119,10 @@ static int rhizome_direct_import_end(struct http_request *hr) case RHIZOME_BUNDLE_STATUS_FAKE: http_request_simple_response(&r->http, 403, "Manifest not signed"); return 0; - case RHIZOME_BUNDLE_STATUS_DONOTWANT: + case RHIZOME_BUNDLE_STATUS_NO_ROOM: http_request_simple_response(&r->http, 403, "Not enough space"); return 0; + case RHIZOME_BUNDLE_STATUS_READONLY: case RHIZOME_BUNDLE_STATUS_DUPLICATE: case RHIZOME_BUNDLE_STATUS_ERROR: break; @@ -729,17 +730,20 @@ void rhizome_direct_http_dispatch(rhizome_direct_sync_request *r) switch (pstatus) { case RHIZOME_PAYLOAD_STATUS_EMPTY: case RHIZOME_PAYLOAD_STATUS_STORED: - break; + goto pstatus_ok; case RHIZOME_PAYLOAD_STATUS_NEW: case RHIZOME_PAYLOAD_STATUS_ERROR: case RHIZOME_PAYLOAD_STATUS_WRONG_SIZE: case RHIZOME_PAYLOAD_STATUS_WRONG_HASH: case RHIZOME_PAYLOAD_STATUS_CRYPTO_FAIL: + case RHIZOME_PAYLOAD_STATUS_TOO_BIG: + case RHIZOME_PAYLOAD_STATUS_EVICTED: goto closeit; - default: - FATALF("pstatus = %d", pstatus); + // No "default" label, so the compiler will warn us if a case is not handled. } - + FATALF("pstatus = %d", pstatus); + pstatus_ok: + ; uint64_t read_ofs; for(read_ofs=0;read_ofsfilesize;){ unsigned char buffer[4096]; diff --git a/rhizome_fetch.c b/rhizome_fetch.c index c8379416..4db942c5 100644 --- a/rhizome_fetch.c +++ b/rhizome_fetch.c @@ -553,10 +553,10 @@ schedule_fetch(struct rhizome_fetch_slot *slot) case RHIZOME_PAYLOAD_STATUS_STORED: RETURN(IMPORTED); case RHIZOME_PAYLOAD_STATUS_TOO_BIG: - case RHIZOME_PAYLOAD_STATUS_UNINITERESTING: + case RHIZOME_PAYLOAD_STATUS_EVICTED: RETURN(DONOTWANT); case RHIZOME_PAYLOAD_STATUS_NEW: - break; + goto status_ok; case RHIZOME_PAYLOAD_STATUS_ERROR: RETURN(WHY("error writing new payload")); case RHIZOME_PAYLOAD_STATUS_WRONG_SIZE: @@ -565,9 +565,11 @@ schedule_fetch(struct rhizome_fetch_slot *slot) RETURN(WHY("payload hash does not match")); case RHIZOME_PAYLOAD_STATUS_CRYPTO_FAIL: RETURN(WHY("payload cannot be encrypted")); - default: - FATALF("status = %d", status); + // No "default" label, so the compiler will warn if a case is not handled. } + FATALF("status = %d", status); +status_ok: + ; } else { strbuf r = strbuf_local(slot->request, sizeof slot->request); strbuf_sprintf(r, "GET /rhizome/manifestbyprefix/%s HTTP/1.0\r\n\r\n", alloca_tohex(slot->bid.binary, slot->prefix_length)); diff --git a/rhizome_restful.c b/rhizome_restful.c index f55a9639..47a96ba7 100644 --- a/rhizome_restful.c +++ b/rhizome_restful.c @@ -63,11 +63,93 @@ static int strn_to_list_token(const char *str, uint64_t *rowidp, const char **af return 1; } +static int http_request_rhizome_response(struct httpd_request *r, uint16_t result, const char *message, const char *payload_status_message) +{ + uint16_t rhizome_result = 0; + switch (r->bundle_status) { + case RHIZOME_BUNDLE_STATUS_NEW: + rhizome_result = 201; + break; + case RHIZOME_BUNDLE_STATUS_SAME: + case RHIZOME_BUNDLE_STATUS_DUPLICATE: + case RHIZOME_BUNDLE_STATUS_OLD: + case RHIZOME_BUNDLE_STATUS_NO_ROOM: + rhizome_result = 200; + break; + case RHIZOME_BUNDLE_STATUS_INVALID: + case RHIZOME_BUNDLE_STATUS_FAKE: + case RHIZOME_BUNDLE_STATUS_INCONSISTENT: + case RHIZOME_BUNDLE_STATUS_READONLY: + rhizome_result = 403; + break; + case RHIZOME_BUNDLE_STATUS_ERROR: + rhizome_result = 500; + break; + } + if (rhizome_result) { + r->http.response.result_extra[0].label = "rhizome_bundle_status_code"; + r->http.response.result_extra[0].value.type = JSON_INTEGER; + r->http.response.result_extra[0].value.u.integer = r->bundle_status; + const char *status_message = rhizome_bundle_status_message(r->bundle_status); + if (status_message) { + r->http.response.result_extra[1].label = "rhizome_bundle_status_message"; + r->http.response.result_extra[1].value.type = JSON_STRING_NULTERM; + r->http.response.result_extra[1].value.u.string.content = status_message; + } + if (rhizome_result > result) { + result = rhizome_result; + message = NULL; + } + } + rhizome_result = 0; + switch (r->payload_status) { + case RHIZOME_PAYLOAD_STATUS_NEW: + rhizome_result = 201; + break; + case RHIZOME_PAYLOAD_STATUS_STORED: + case RHIZOME_PAYLOAD_STATUS_EMPTY: + case RHIZOME_PAYLOAD_STATUS_TOO_BIG: + case RHIZOME_PAYLOAD_STATUS_EVICTED: + rhizome_result = 200; + break; + case RHIZOME_PAYLOAD_STATUS_WRONG_SIZE: + case RHIZOME_PAYLOAD_STATUS_WRONG_HASH: + case RHIZOME_PAYLOAD_STATUS_CRYPTO_FAIL: + rhizome_result = 403; + break; + case RHIZOME_PAYLOAD_STATUS_ERROR: + rhizome_result = 500; + break; + } + if (rhizome_result) { + r->http.response.result_extra[2].label = "rhizome_payload_status_code"; + r->http.response.result_extra[2].value.type = JSON_INTEGER; + r->http.response.result_extra[2].value.u.integer = r->payload_status; + const char *status_message = payload_status_message ? payload_status_message : rhizome_payload_status_message(r->payload_status); + if (status_message) { + r->http.response.result_extra[3].label = "rhizome_payload_status_message"; + r->http.response.result_extra[3].value.type = JSON_STRING_NULTERM; + r->http.response.result_extra[3].value.u.string.content = status_message; + } + if (rhizome_result > result) { + result = rhizome_result; + message = NULL; + } + } + if (result == 0) { + result = 500; + message = NULL; + } + http_request_simple_response(&r->http, result, message ? message : result == 403 ? "Rhizome operation failed" : NULL); + return result; +} + static HTTP_CONTENT_GENERATOR restful_rhizome_bundlelist_json_content; int restful_rhizome_bundlelist_json(httpd_request *r, const char *remainder) { r->http.response.header.content_type = CONTENT_TYPE_JSON; + r->http.render_extra_headers = render_manifest_headers; if (!is_rhizome_http_enabled()) return 403; if (*remainder) @@ -242,6 +324,7 @@ static int insert_mime_part_body(struct http_request *, char *, size_t); int restful_rhizome_insert(httpd_request *r, const char *remainder) { r->http.response.header.content_type = CONTENT_TYPE_JSON; + r->http.render_extra_headers = render_manifest_headers; if (*remainder) return 404; if (!is_rhizome_http_enabled()) @@ -300,8 +383,7 @@ static int insert_make_manifest(httpd_request *r) return 0; // fall through case 1: - http_request_simple_response(&r->http, 403, "Malformed manifest"); - return 403; + return http_request_rhizome_response(r, 403, "Malformed manifest", NULL); default: WHYF("rhizome_manifest_parse() returned %d", n); break; @@ -355,17 +437,17 @@ static int insert_mime_part_header(struct http_request *hr, const struct mime_pa rhizome_manifest_set_name_from_path(r->manifest, h->content_disposition.filename); // Start writing the payload content into the Rhizome store. Note: r->manifest->filesize can be // RHIZOME_SIZE_UNSET at this point, if the manifest did not contain a 'filesize' field. - r->u.insert.payload_status = rhizome_write_open_manifest(&r->u.insert.write, r->manifest); + r->payload_status = rhizome_write_open_manifest(&r->u.insert.write, r->manifest); r->u.insert.payload_size = 0; - switch (r->u.insert.payload_status) { + switch (r->payload_status) { case RHIZOME_PAYLOAD_STATUS_ERROR: - WHYF("rhizome_write_open_manifest() returned %d", r->u.insert.payload_status); + WHYF("rhizome_write_open_manifest() returned %d", r->payload_status); return 500; case RHIZOME_PAYLOAD_STATUS_STORED: // TODO: initialise payload hash so it can be compared with stored payload break; default: - break; + break; // r->payload_status gets dealt with later } } else @@ -395,7 +477,7 @@ static int insert_mime_part_body(struct http_request *hr, char *buf, size_t len) } else if (r->u.insert.current_part == PART_PAYLOAD) { r->u.insert.payload_size += len; - switch (r->u.insert.payload_status) { + switch (r->payload_status) { case RHIZOME_PAYLOAD_STATUS_NEW: if (rhizome_write_buffer(&r->u.insert.write, (unsigned char *)buf, len) == -1) return 500; @@ -445,19 +527,25 @@ static int insert_mime_part_end(struct http_request *hr) WHY("rhizome_fill_manifest() failed"); return 500; } - if (r->manifest->is_journal) { - http_request_simple_response(&r->http, 403, "Insert not supported for journals"); - return 403; - } + if (r->manifest->is_journal) + return http_request_rhizome_response(r, 403, "Insert not supported for journals", NULL); assert(r->manifest != NULL); } else if (r->u.insert.current_part == PART_PAYLOAD) { r->u.insert.received_payload = 1; - if (r->u.insert.payload_status == RHIZOME_PAYLOAD_STATUS_NEW) - r->u.insert.payload_status = rhizome_finish_write(&r->u.insert.write); - if (r->u.insert.payload_status == RHIZOME_PAYLOAD_STATUS_ERROR) { - WHYF("rhizome_finish_write() returned status = %d", r->u.insert.payload_status); - return 500; + switch (r->payload_status) { + case RHIZOME_PAYLOAD_STATUS_NEW: + r->payload_status = rhizome_finish_write(&r->u.insert.write); + if (r->payload_status == RHIZOME_PAYLOAD_STATUS_ERROR) { + WHYF("rhizome_finish_write() returned status = %d", r->payload_status); + return 500; + } + break; + case RHIZOME_PAYLOAD_STATUS_STORED: + // TODO: finish calculating payload hash and compare it with stored payload + break; + default: + break; } } else FATALF("current_part = %s", alloca_str_toprint(r->u.insert.current_part)); @@ -475,40 +563,40 @@ static int restful_rhizome_insert_end(struct http_request *hr) // Fill in the missing manifest fields and ensure payload and manifest are consistent. assert(r->manifest != NULL); assert(r->u.insert.write.file_length != RHIZOME_SIZE_UNSET); - switch (r->u.insert.payload_status) { - case RHIZOME_PAYLOAD_STATUS_ERROR: - return 500; + int status_valid = 0; + switch (r->payload_status) { case RHIZOME_PAYLOAD_STATUS_NEW: + status_valid = 1; if (r->manifest->filesize == RHIZOME_SIZE_UNSET) rhizome_manifest_set_filesize(r->manifest, r->u.insert.write.file_length); // fall through case RHIZOME_PAYLOAD_STATUS_STORED: + status_valid = 1; // TODO: check that stored hash matches received payload's hash // fall through case RHIZOME_PAYLOAD_STATUS_EMPTY: + status_valid = 1; assert(r->manifest->filesize != RHIZOME_SIZE_UNSET); if (r->u.insert.payload_size == r->manifest->filesize) break; - // fall through case RHIZOME_PAYLOAD_STATUS_WRONG_SIZE: + r->payload_status = RHIZOME_PAYLOAD_STATUS_WRONG_SIZE; + status_valid = 1; { strbuf msg = strbuf_alloca(200); strbuf_sprintf(msg, "Payload size (%"PRIu64") contradicts manifest (filesize=%"PRIu64")", r->u.insert.payload_size, r->manifest->filesize); - http_request_simple_response(&r->http, 403, strbuf_str(msg)); - return 403; + return http_request_rhizome_response(r, 403, NULL, strbuf_str(msg)); } case RHIZOME_PAYLOAD_STATUS_TOO_BIG: - case RHIZOME_PAYLOAD_STATUS_UNINITERESTING: - http_request_simple_response(&r->http, 403, "Not enough space"); - return 403; + case RHIZOME_PAYLOAD_STATUS_EVICTED: case RHIZOME_PAYLOAD_STATUS_WRONG_HASH: - http_request_simple_response(&r->http, 403, "Payload hash contradicts manifest"); - return 403; case RHIZOME_PAYLOAD_STATUS_CRYPTO_FAIL: - http_request_simple_response(&r->http, 403, "Missing bundle secret"); - return 403; - default: - FATALF("payload_status = %d", r->u.insert.payload_status); + case RHIZOME_PAYLOAD_STATUS_ERROR: + return http_request_rhizome_response(r, 403, NULL, NULL); + } + if (!status_valid) { + WHYF("r->payload_status = %d", r->payload_status); + return http_request_rhizome_response(r, 500, NULL, NULL); } // Finalise the manifest and add it to the store. if (r->manifest->filesize) { @@ -526,35 +614,36 @@ static int restful_rhizome_insert_end(struct http_request *hr) return 403; } rhizome_manifest *mout = NULL; - int result; - switch (rhizome_manifest_finalise(r->manifest, &mout, !r->u.insert.force_new)) { + r->bundle_status = rhizome_manifest_finalise(r->manifest, &mout, !r->u.insert.force_new); + int result = 500; + switch (r->bundle_status) { case RHIZOME_BUNDLE_STATUS_NEW: - result = 201; if (mout && mout != r->manifest) rhizome_manifest_free(mout); - mout = NULL; + result = 201; break; case RHIZOME_BUNDLE_STATUS_SAME: case RHIZOME_BUNDLE_STATUS_OLD: case RHIZOME_BUNDLE_STATUS_DUPLICATE: + if (mout && mout != r->manifest) { + rhizome_manifest_free(r->manifest); + r->manifest = mout; + } result = 200; break; case RHIZOME_BUNDLE_STATUS_INVALID: - result = 403; - break; + case RHIZOME_BUNDLE_STATUS_FAKE: + case RHIZOME_BUNDLE_STATUS_INCONSISTENT: + case RHIZOME_BUNDLE_STATUS_NO_ROOM: + case RHIZOME_BUNDLE_STATUS_READONLY: case RHIZOME_BUNDLE_STATUS_ERROR: - default: - result = 500; - break; + if (mout && mout != r->manifest) + rhizome_manifest_free(mout); + return http_request_rhizome_response(r, 0, NULL, NULL); } - if (mout && mout != r->manifest) { - rhizome_manifest_free(r->manifest); - r->manifest = mout; - } - if (result >= 400) - return result; + if (result == 500) + FATALF("rhizome_manifest_finalise() returned status = %d", r->bundle_status); rhizome_authenticate_author(r->manifest); - r->http.render_extra_headers = render_manifest_headers; http_request_response_static(&r->http, result, "rhizome-manifest/text", (const char *)r->manifest->manifestdata, r->manifest->manifest_all_bytes ); @@ -568,6 +657,7 @@ static HTTP_HANDLER restful_rhizome_bid_decrypted_bin; int restful_rhizome_(httpd_request *r, const char *remainder) { r->http.response.header.content_type = CONTENT_TYPE_JSON; + r->http.render_extra_headers = render_manifest_headers; if (!is_rhizome_http_enabled()) return 403; HTTP_HANDLER *handler = NULL; @@ -598,15 +688,16 @@ int restful_rhizome_(httpd_request *r, const char *remainder) if (ret == -1) { rhizome_manifest_free(r->manifest); r->manifest = NULL; + r->bundle_status = RHIZOME_BUNDLE_STATUS_ERROR; return 500; } if (ret == 0) { rhizome_authenticate_author(r->manifest); - r->http.render_extra_headers = render_manifest_headers; + r->bundle_status = RHIZOME_BUNDLE_STATUS_SAME; } else { + r->bundle_status = RHIZOME_BUNDLE_STATUS_NEW; rhizome_manifest_free(r->manifest); r->manifest = NULL; - assert(r->http.render_extra_headers == NULL); } ret = handler(r, remainder); return ret; @@ -614,8 +705,10 @@ int restful_rhizome_(httpd_request *r, const char *remainder) static int restful_rhizome_bid_rhm(httpd_request *r, const char *remainder) { - if (*remainder || r->manifest == NULL) + if (*remainder) return 404; + if (r->manifest == NULL) + return http_request_rhizome_response(r, 404, NULL, NULL); http_request_response_static(&r->http, 200, "rhizome-manifest/text", (const char *)r->manifest->manifestdata, r->manifest->manifest_all_bytes ); @@ -624,8 +717,10 @@ static int restful_rhizome_bid_rhm(httpd_request *r, const char *remainder) static int restful_rhizome_bid_raw_bin(httpd_request *r, const char *remainder) { - if (*remainder || r->manifest == NULL) + if (*remainder) return 404; + if (r->manifest == NULL) + return http_request_rhizome_response(r, 404, NULL, NULL); if (r->manifest->filesize == 0) { http_request_response_static(&r->http, 200, CONTENT_TYPE_BLOB, "", 0); return 1; @@ -639,8 +734,10 @@ static int restful_rhizome_bid_raw_bin(httpd_request *r, const char *remainder) static int restful_rhizome_bid_decrypted_bin(httpd_request *r, const char *remainder) { - if (*remainder || r->manifest == NULL) + if (*remainder) return 404; + if (r->manifest == NULL) + return http_request_rhizome_response(r, 404, NULL, NULL); if (r->manifest->filesize == 0) { // TODO use Content Type from manifest (once it is implemented) http_request_response_static(&r->http, 200, CONTENT_TYPE_BLOB, "", 0); @@ -685,22 +782,22 @@ int rhizome_response_content_init_filehash(httpd_request *r, const rhizome_fileh r->u.read_state.blob_fd = -1; assert(r->finalise_union == NULL); r->finalise_union = finalise_union_read_state; - enum rhizome_payload_status status = rhizome_open_read(&r->u.read_state, hash); - switch (status) { + r->payload_status = rhizome_open_read(&r->u.read_state, hash); + switch (r->payload_status) { case RHIZOME_PAYLOAD_STATUS_EMPTY: case RHIZOME_PAYLOAD_STATUS_STORED: - break; + return rhizome_response_content_init_read_state(r); case RHIZOME_PAYLOAD_STATUS_NEW: - return 404; + return 403; case RHIZOME_PAYLOAD_STATUS_ERROR: case RHIZOME_PAYLOAD_STATUS_WRONG_SIZE: case RHIZOME_PAYLOAD_STATUS_WRONG_HASH: case RHIZOME_PAYLOAD_STATUS_CRYPTO_FAIL: + case RHIZOME_PAYLOAD_STATUS_TOO_BIG: + case RHIZOME_PAYLOAD_STATUS_EVICTED: return -1; - default: - FATALF("status = %d", status); } - return rhizome_response_content_init_read_state(r); + FATALF("rhizome_open_read() returned status = %d", r->payload_status); } int rhizome_response_content_init_payload(httpd_request *r, rhizome_manifest *m) @@ -709,22 +806,22 @@ int rhizome_response_content_init_payload(httpd_request *r, rhizome_manifest *m) r->u.read_state.blob_fd = -1; assert(r->finalise_union == NULL); r->finalise_union = finalise_union_read_state; - enum rhizome_payload_status status = rhizome_open_decrypt_read(m, &r->u.read_state); - switch (status) { + r->payload_status = rhizome_open_decrypt_read(m, &r->u.read_state); + switch (r->payload_status) { case RHIZOME_PAYLOAD_STATUS_EMPTY: case RHIZOME_PAYLOAD_STATUS_STORED: - break; + return rhizome_response_content_init_read_state(r); case RHIZOME_PAYLOAD_STATUS_NEW: - return 404; + case RHIZOME_PAYLOAD_STATUS_CRYPTO_FAIL: + return 403; case RHIZOME_PAYLOAD_STATUS_ERROR: case RHIZOME_PAYLOAD_STATUS_WRONG_SIZE: case RHIZOME_PAYLOAD_STATUS_WRONG_HASH: - case RHIZOME_PAYLOAD_STATUS_CRYPTO_FAIL: + case RHIZOME_PAYLOAD_STATUS_TOO_BIG: + case RHIZOME_PAYLOAD_STATUS_EVICTED: return -1; - default: - FATALF("status = %d", status); } - return rhizome_response_content_init_read_state(r); + FATALF("rhizome_open_decrypt_read() returned status = %d", r->payload_status); } int rhizome_payload_content(struct http_request *hr, unsigned char *buf, size_t bufsz, struct http_content_generator_result *result) @@ -758,49 +855,58 @@ int rhizome_payload_content(struct http_request *hr, unsigned char *buf, size_t static void render_manifest_headers(struct http_request *hr, strbuf sb) { httpd_request *r = (httpd_request *) hr; + const char *status_message; + if ((status_message = rhizome_bundle_status_message(r->bundle_status))) { + strbuf_sprintf(sb, "Serval-Rhizome-Result-Bundle-Status-Code: %d\r\n", r->bundle_status); + strbuf_sprintf(sb, "Serval-Rhizome-Result-Bundle-Status-Message: %s\r\n", status_message); + } + if ((status_message = rhizome_payload_status_message(r->payload_status))) { + strbuf_sprintf(sb, "Serval-Rhizome-Result-Payload-Status-Code: %d\r\n", r->payload_status); + strbuf_sprintf(sb, "Serval-Rhizome-Result-Payload-Status-Message: %s\r\n", status_message); + } rhizome_manifest *m = r->manifest; - strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Id: %s\r\n", alloca_tohex_rhizome_bid_t(m->cryptoSignPublic)); - strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Version: %"PRIu64"\r\n", m->version); - strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Filesize: %"PRIu64"\r\n", m->filesize); - if (m->filesize != 0) - strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Filehash: %s\r\n", alloca_tohex_rhizome_filehash_t(m->filehash)); - if (m->has_sender) - strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Sender: %s\r\n", alloca_tohex_sid_t(m->sender)); - if (m->has_recipient) - strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Recipient: %s\r\n", alloca_tohex_sid_t(m->recipient)); - if (m->has_bundle_key) - strbuf_sprintf(sb, "Serval-Rhizome-Bundle-BK: %s\r\n", alloca_tohex_rhizome_bk_t(m->bundle_key)); - switch (m->payloadEncryption) { - case PAYLOAD_CRYPT_UNKNOWN: - break; - case PAYLOAD_CLEAR: - strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Crypt: 0\r\n"); - break; - case PAYLOAD_ENCRYPTED: - strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Crypt: 1\r\n"); - break; + if (m) { + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Id: %s\r\n", alloca_tohex_rhizome_bid_t(m->cryptoSignPublic)); + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Version: %"PRIu64"\r\n", m->version); + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Filesize: %"PRIu64"\r\n", m->filesize); + if (m->filesize != 0) + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Filehash: %s\r\n", alloca_tohex_rhizome_filehash_t(m->filehash)); + if (m->has_sender) + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Sender: %s\r\n", alloca_tohex_sid_t(m->sender)); + if (m->has_recipient) + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Recipient: %s\r\n", alloca_tohex_sid_t(m->recipient)); + if (m->has_bundle_key) + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-BK: %s\r\n", alloca_tohex_rhizome_bk_t(m->bundle_key)); + switch (m->payloadEncryption) { + case PAYLOAD_CRYPT_UNKNOWN: + break; + case PAYLOAD_CLEAR: + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Crypt: 0\r\n"); + break; + case PAYLOAD_ENCRYPTED: + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Crypt: 1\r\n"); + break; + } + if (m->is_journal) + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Tail: %"PRIu64"\r\n", m->tail); + if (m->has_date) + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Date: %"PRIu64"\r\n", m->date); + if (m->name) { + strbuf_puts(sb, "Serval-Rhizome-Bundle-Name: "); + strbuf_append_quoted_string(sb, m->name); + strbuf_puts(sb, "\r\n"); + } + if (m->service) + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Service: %s\r\n", m->service); + assert(m->authorship != AUTHOR_LOCAL); + if (m->authorship == AUTHOR_AUTHENTIC) + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Author: %s\r\n", alloca_tohex_sid_t(m->author)); + if (m->haveSecret) { + char secret[RHIZOME_BUNDLE_KEY_STRLEN + 1]; + rhizome_bytes_to_hex_upper(m->cryptoSignSecret, secret, RHIZOME_BUNDLE_KEY_BYTES); + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Secret: %s\r\n", secret); + } + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Rowid: %"PRIu64"\r\n", m->rowid); + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Inserttime: %"PRIu64"\r\n", m->inserttime); } - if (m->is_journal) - strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Tail: %"PRIu64"\r\n", m->tail); - if (m->has_date) - strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Date: %"PRIu64"\r\n", m->date); - if (m->name) { - strbuf_puts(sb, "Serval-Rhizome-Bundle-Name: "); - strbuf_append_quoted_string(sb, m->name); - strbuf_puts(sb, "\r\n"); - } - if (m->service) - strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Service: %s\r\n", m->service); - assert(m->authorship != AUTHOR_LOCAL); - if (m->authorship == AUTHOR_AUTHENTIC) - strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Author: %s\r\n", alloca_tohex_sid_t(m->author)); - assert(m->haveSecret); - { - char secret[RHIZOME_BUNDLE_KEY_STRLEN + 1]; - rhizome_bytes_to_hex_upper(m->cryptoSignSecret, secret, RHIZOME_BUNDLE_KEY_BYTES); - strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Secret: %s\r\n", secret); - } - strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Rowid: %"PRIu64"\r\n", m->rowid); - strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Inserttime: %"PRIu64"\r\n", m->inserttime); } - diff --git a/rhizome_store.c b/rhizome_store.c index 0a93261a..08154dd0 100644 --- a/rhizome_store.c +++ b/rhizome_store.c @@ -296,7 +296,7 @@ static enum rhizome_payload_status store_make_space(uint64_t bytes, struct rhizo DEBUGF("Not enough space for %"PRIu64". Used; %"PRIu64" = %"PRIu64" + %"PRIu64" * (%"PRIu64" - %"PRIu64"), Limit; %"PRIu64, bytes, db_used, external_bytes, db_page_size, db_page_count, db_free_page_count, limit); - return RHIZOME_PAYLOAD_STATUS_UNINITERESTING; + return RHIZOME_PAYLOAD_STATUS_EVICTED; } int rhizome_store_cleanup(struct rhizome_cleanup_report *report) @@ -963,21 +963,23 @@ enum rhizome_payload_status rhizome_store_payload_file(rhizome_manifest *m, cons struct rhizome_write write; bzero(&write, sizeof(write)); enum rhizome_payload_status status = rhizome_write_open_manifest(&write, m); + int status_ok = 0; switch (status) { case RHIZOME_PAYLOAD_STATUS_EMPTY: case RHIZOME_PAYLOAD_STATUS_NEW: + status_ok = 1; break; case RHIZOME_PAYLOAD_STATUS_STORED: case RHIZOME_PAYLOAD_STATUS_TOO_BIG: - case RHIZOME_PAYLOAD_STATUS_UNINITERESTING: + case RHIZOME_PAYLOAD_STATUS_EVICTED: case RHIZOME_PAYLOAD_STATUS_ERROR: case RHIZOME_PAYLOAD_STATUS_WRONG_SIZE: case RHIZOME_PAYLOAD_STATUS_WRONG_HASH: case RHIZOME_PAYLOAD_STATUS_CRYPTO_FAIL: return status; - default: - FATALF("status = %d", status); } + if (!status_ok) + FATALF("rhizome_write_open_manifest() returned status = %d", status); if (rhizome_write_file(&write, filepath) == -1) { rhizome_fail_write(&write); return RHIZOME_PAYLOAD_STATUS_ERROR; @@ -987,26 +989,24 @@ enum rhizome_payload_status rhizome_store_payload_file(rhizome_manifest *m, cons case RHIZOME_PAYLOAD_STATUS_EMPTY: assert(write.file_length == 0); assert(m->filesize == 0); - break; + return status; case RHIZOME_PAYLOAD_STATUS_NEW: assert(m->filesize == write.file_length); if (m->has_filehash) assert(cmp_rhizome_filehash_t(&m->filehash, &write.id) == 0); else rhizome_manifest_set_filehash(m, &write.id); - break; + return status; case RHIZOME_PAYLOAD_STATUS_ERROR: case RHIZOME_PAYLOAD_STATUS_STORED: case RHIZOME_PAYLOAD_STATUS_WRONG_SIZE: case RHIZOME_PAYLOAD_STATUS_WRONG_HASH: case RHIZOME_PAYLOAD_STATUS_CRYPTO_FAIL: case RHIZOME_PAYLOAD_STATUS_TOO_BIG: - case RHIZOME_PAYLOAD_STATUS_UNINITERESTING: - break; - default: - FATALF("status = %d", status); + case RHIZOME_PAYLOAD_STATUS_EVICTED: + return status; } - return status; + FATALF("rhizome_finish_write() returned status = %d", status); } /* Return RHIZOME_PAYLOAD_STATUS_STORED if file blob found, RHIZOME_PAYLOAD_STATUS_NEW if not found. diff --git a/testdefs_rhizome.sh b/testdefs_rhizome.sh index c3e3ff38..443cc2fe 100644 --- a/testdefs_rhizome.sh +++ b/testdefs_rhizome.sh @@ -28,6 +28,8 @@ rexp_crypt='[01]' rexp_date='[0-9]\{1,\}' rexp_rowid='[0-9]\{1,\}' +BID_NONEXISTENT=0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF + assert_manifest_complete() { local manifest="$1" tfw_cat -v "$manifest" @@ -552,3 +554,11 @@ rhizome_add_bundles() { [ "${ROWID[$n]}" -gt "${ROWID_MAX:-0}" ] && ROWID_MAX=${ROWID[$n]} done } + +rhizome_delete_payload_blobs() { + local filehash + for filehash; do + assert --message="Rhizome external blob file exists, filehash=$filehash" [ -e "$SERVALINSTANCE_PATH/blob/$filehash" ] + rm -f "$SERVALINSTANCE_PATH/blob/$filehash" + done +} diff --git a/tests/rhizomerestful b/tests/rhizomerestful index 1c8c3944..e4e3a7f2 100755 --- a/tests/rhizomerestful +++ b/tests/rhizomerestful @@ -225,19 +225,19 @@ test_RhizomeNewSince() { } assert_http_response_headers() { - local n=$1 - assertGrep --matches=1 http.headers$n "^Serval-Rhizome-Bundle-Id: ${BID[$n]}$CR\$" - assertGrep --matches=1 http.headers$n "^Serval-Rhizome-Bundle-Version: ${VERSION[$n]}$CR\$" - assertGrep --matches=1 http.headers$n "^Serval-Rhizome-Bundle-Filesize: ${SIZE[$n]}$CR\$" - assertGrep --matches=1 http.headers$n "^Serval-Rhizome-Bundle-Filehash: ${HASH[$n]}$CR\$" - assertGrep --matches=1 http.headers$n "^Serval-Rhizome-Bundle-BK: ${BK[$n]}$CR\$" - assertGrep --matches=1 http.headers$n "^Serval-Rhizome-Bundle-Date: ${DATE[$n]}$CR\$" - assertGrep --matches=1 http.headers$n "^Serval-Rhizome-Bundle-Name: \"${NAME[$n]}\"$CR\$" - assertGrep --matches=1 http.headers$n "^Serval-Rhizome-Bundle-Service: file$CR\$" - assertGrep --matches=1 http.headers$n "^Serval-Rhizome-Bundle-Author: ${AUTHOR[$n]}$CR\$" - assertGrep --matches=1 http.headers$n "^Serval-Rhizome-Bundle-Secret: ${SECRET[$n]}$CR\$" - assertGrep --matches=1 http.headers$n "^Serval-Rhizome-Bundle-Inserttime: ${INSERTTIME[$n]}$CR\$" - assertGrep --matches=1 http.headers$n "^Serval-Rhizome-Bundle-Rowid: ${ROWID[$n]}$CR\$" + local file="$1" + assertGrep --matches=1 "$file" "^Serval-Rhizome-Bundle-Id: ${BID[$n]}$CR\$" + assertGrep --matches=1 "$file" "^Serval-Rhizome-Bundle-Version: ${VERSION[$n]}$CR\$" + assertGrep --matches=1 "$file" "^Serval-Rhizome-Bundle-Filesize: ${SIZE[$n]}$CR\$" + assertGrep --matches=1 "$file" "^Serval-Rhizome-Bundle-Filehash: ${HASH[$n]}$CR\$" + assertGrep --matches=1 "$file" "^Serval-Rhizome-Bundle-BK: ${BK[$n]}$CR\$" + assertGrep --matches=1 "$file" "^Serval-Rhizome-Bundle-Date: ${DATE[$n]}$CR\$" + assertGrep --matches=1 "$file" "^Serval-Rhizome-Bundle-Name: \"${NAME[$n]}\"$CR\$" + assertGrep --matches=1 "$file" "^Serval-Rhizome-Bundle-Service: file$CR\$" + assertGrep --matches=1 "$file" "^Serval-Rhizome-Bundle-Author: ${AUTHOR[$n]}$CR\$" + assertGrep --matches=1 "$file" "^Serval-Rhizome-Bundle-Secret: ${SECRET[$n]}$CR\$" + assertGrep --matches=1 "$file" "^Serval-Rhizome-Bundle-Inserttime: ${INSERTTIME[$n]}$CR\$" + assertGrep --matches=1 "$file" "^Serval-Rhizome-Bundle-Rowid: ${ROWID[$n]}$CR\$" } doc_RhizomeManifest="HTTP RESTful fetch Rhizome manifest" @@ -258,10 +258,32 @@ test_RhizomeManifest() { done for n in 0 1 2; do assert diff file$n.manifest bundle$n.rhm - assert_http_response_headers $n + assert_http_response_headers http.headers$n done } +doc_RhizomeManifestNonexist="HTTP RESTful fetch non-existent Rhizome manifest" +setup_RhizomeManifestNonexist() { + setup +} +test_RhizomeManifestNonexist() { + executeOk curl \ + --silent --show-error --write-out '%{http_code}' \ + --output http.content \ + --dump-header http.headers \ + --basic --user harry:potter \ + "http://$addr_localhost:$PORTA/restful/rhizome/$BID_NONEXISTENT.rhm" + tfw_cat http.headers http.content + assertStdoutIs 404 + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Code: 0$CR\$" + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Message: .*bundle new to store.*$CR\$" + assertGrep --matches=0 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Code:" + assertGrep --matches=0 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Message:" + assertJq http.content 'contains({"http_status_code": 404})' + assertJq http.content 'contains({"rhizome_bundle_status_code": 0})' + assertJqGrep --ignore-case http.content '.rhizome_bundle_status_message' "bundle new to store" +} + doc_RhizomePayloadRaw="HTTP RESTful fetch Rhizome raw payload" setup_RhizomePayloadRaw() { setup @@ -280,10 +302,65 @@ test_RhizomePayloadRaw() { done for n in 0 1 2 3; do assert cmp raw$n raw.bin$n - assert_http_response_headers $n + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Code: 1$CR\$" + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Message: .*bundle already in store.*$CR\$" + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Code: 2$CR\$" + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Message: .*payload already in store.*$CR\$" + assert_http_response_headers http.headers$n done } +doc_RhizomePayloadRawNonexistManifest="HTTP RESTful fetch Rhizome raw payload for non-existent manifest" +setup_RhizomePayloadRawNonexistManifest() { + setup +} +test_RhizomePayloadRawNonexistManifest() { + executeOk curl \ + --silent --show-error --write-out '%{http_code}' \ + --output http.content \ + --dump-header http.headers \ + --basic --user harry:potter \ + "http://$addr_localhost:$PORTA/restful/rhizome/$BID_NONEXISTENT/raw.bin" + tfw_cat http.headers http.content + assertStdoutIs 404 + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Code: 0$CR\$" + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Message: .*bundle new to store.*$CR\$" + assertGrep --matches=0 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Code:" + assertGrep --matches=0 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Message:" + assertJq http.content 'contains({"http_status_code": 404})' + assertJq http.content 'contains({"rhizome_bundle_status_code": 0})' + assertJqGrep --ignore-case http.content '.rhizome_bundle_status_message' "bundle new to store" +} + +doc_RhizomePayloadRawNonexistPayload="HTTP RESTful fetch non-existent Rhizome raw payload" +setup_RhizomePayloadRawNonexistPayload() { + set_extra_config() { + executeOk_servald config set rhizome.max_blob_size 0 + } + setup + rhizome_add_bundles $SIDA 0 0 + rhizome_delete_payload_blobs "${HASH[0]}" +} +test_RhizomePayloadRawNonexistPayload() { + executeOk curl \ + --silent --show-error --write-out '%{http_code}' \ + --output http.content \ + --dump-header http.headers \ + --basic --user harry:potter \ + "http://$addr_localhost:$PORTA/restful/rhizome/${BID[0]}/raw.bin" + tfw_cat http.headers http.content + assertStdoutIs 403 + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Code: 1$CR\$" + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Message: .*bundle already in store.*$CR\$" + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Code: 1$CR\$" + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Message: .*payload new to store.*$CR\$" + assertJq http.content 'contains({"http_status_code": 403})' + assertJq http.content 'contains({"rhizome_bundle_status_code": 1})' + assertJqGrep --ignore-case http.content '.rhizome_bundle_status_message' "bundle already in store" + assertJq http.content 'contains({"rhizome_payload_status_code": 1})' + assertJqGrep --ignore-case http.content '.rhizome_payload_status_message' "payload new to store" +} + doc_RhizomePayloadDecrypted="HTTP RESTful fetch Rhizome decrypted payload" setup_RhizomePayloadDecrypted() { setup @@ -302,10 +379,91 @@ test_RhizomePayloadDecrypted() { done for n in 0 1 2 3; do assert cmp file$n decrypted.bin$n - assert_http_response_headers $n + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Code: 1$CR\$" + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Message: .*bundle already in store.*$CR\$" + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Code: 2$CR\$" + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Message: .*payload already in store.*$CR\$" + assert_http_response_headers http.headers$n done } +doc_RhizomePayloadDecryptedForeign="HTTP RESTful cannot fetch foreign Rhizome decrypted payload" +setup_RhizomePayloadDecryptedForeign() { + setup + rhizome_add_bundles --encrypted $SIDA 0 0 + set_instance +B + create_single_identity + rhizome_add_bundles --encrypted $SIDB 1 1 + executeOk_servald rhizome export manifest "${BID[1]}" file1.manifest + set_instance +A + executeOk_servald rhizome import bundle raw1 file1.manifest +} +test_RhizomePayloadDecryptedForeign() { + executeOk curl \ + --silent --show-error --write-out '%{http_code}' \ + --output decrypted.bin$n \ + --dump-header http.headers$n \ + --basic --user harry:potter \ + "http://$addr_localhost:$PORTA/restful/rhizome/${BID[1]}/decrypted.bin" + tfw_cat http.headers$n decrypted.bin$n + assertStdoutIs 403 + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Code: 1$CR\$" + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Message: .*bundle already in store.*$CR\$" + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Code: 5$CR\$" + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Message: .*incorrect bundle secret*$CR\$" +} + +doc_RhizomePayloadDecryptedNonexistManifest="HTTP RESTful fetch Rhizome decrypted payload for non-existent manifest" +setup_RhizomePayloadDecryptedNonexistManifest() { + setup +} +test_RhizomePayloadDecryptedNonexistManifest() { + executeOk curl \ + --silent --show-error --write-out '%{http_code}' \ + --output http.content \ + --dump-header http.headers \ + --basic --user harry:potter \ + "http://$addr_localhost:$PORTA/restful/rhizome/$BID_NONEXISTENT/decrypted.bin" + tfw_cat http.headers http.content + assertStdoutIs 404 + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Code: 0$CR\$" + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Message: .*bundle new to store.*$CR\$" + assertGrep --matches=0 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Code:" + assertGrep --matches=0 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Message:" + assertJq http.content 'contains({"http_status_code": 404})' + assertJq http.content 'contains({"rhizome_bundle_status_code": 0})' + assertJqGrep --ignore-case http.content '.rhizome_bundle_status_message' "bundle new to store" +} + +doc_RhizomePayloadDecryptedNonexistPayload="HTTP RESTful fetch non-existent Rhizome decrypted payload" +setup_RhizomePayloadDecryptedNonexistPayload() { + set_extra_config() { + executeOk_servald config set rhizome.max_blob_size 0 + } + setup + rhizome_add_bundles $SIDA 0 0 + rhizome_delete_payload_blobs "${HASH[0]}" +} +test_RhizomePayloadDecryptedNonexistPayload() { + executeOk curl \ + --silent --show-error --write-out '%{http_code}' \ + --output http.content \ + --dump-header http.headers \ + --basic --user harry:potter \ + "http://$addr_localhost:$PORTA/restful/rhizome/${BID[0]}/decrypted.bin" + tfw_cat http.headers http.content + assertStdoutIs 403 + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Code: 1$CR\$" + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Message: .*bundle already in store.*$CR\$" + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Code: 1$CR\$" + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Message: .*payload new to store.*$CR\$" + assertJq http.content 'contains({"http_status_code": 403})' + assertJq http.content 'contains({"rhizome_bundle_status_code": 1})' + assertJqGrep --ignore-case http.content '.rhizome_bundle_status_message' "bundle already in store" + assertJq http.content 'contains({"rhizome_payload_status_code": 1})' + assertJqGrep --ignore-case http.content '.rhizome_payload_status_message' "payload new to store" +} + extract_http_header() { local __var="$1" local __headerfile="$2" @@ -419,15 +577,19 @@ test_RhizomeInsert() { execute curl \ --silent --show-error --write-out '%{http_code}' \ --output nfile$n.manifest \ - --dump-header http.header$n \ + --dump-header http.headers$n \ --basic --user harry:potter \ --form "manifest=@nmanifest$n;type=rhizome-manifest/text" \ --form "payload=@nfile$n;filename=\"nfile$n\"" \ "http://$addr_localhost:$PORTA/restful/rhizome/insert" - tfw_cat http.header$n nfile$n.manifest + tfw_cat http.headers$n nfile$n.manifest assertExitStatus == 0 if [ -n "${author[$n]}" ]; then assertStdoutIs 201 + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Code: 0$CR\$" + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Message: .*bundle new to store.*$CR\$" + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Code: 1$CR\$" + assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Message: .*payload new to store.*$CR\$" else assertStdoutIs 403 assertJq nfile$n.manifest 'contains({"http_status_code": 403})' @@ -460,6 +622,10 @@ test_RhizomeInsertAnon() { tfw_cat http.header ifile2.manifest assertExitStatus == 0 assertStdoutIs 201 + assertGrep --matches=1 --ignore-case http.header "^Serval-Rhizome-Result-Bundle-Status-Code: 0$CR\$" + assertGrep --matches=1 --ignore-case http.header "^Serval-Rhizome-Result-Bundle-Status-Message: .*bundle new to store.*$CR\$" + assertGrep --matches=1 --ignore-case http.header "^Serval-Rhizome-Result-Payload-Status-Code: 1$CR\$" + assertGrep --matches=1 --ignore-case http.header "^Serval-Rhizome-Result-Payload-Status-Message: .*payload new to store.*$CR\$" executeOk_servald rhizome list assert_rhizome_list --fromhere=0 --manifest=ifile2.manifest file2 } @@ -482,6 +648,10 @@ test_RhizomeInsertEmpty() { tfw_cat http.header empty.manifest assertExitStatus == 0 assertStdoutIs 201 + assertGrep --matches=1 --ignore-case http.header "^Serval-Rhizome-Result-Bundle-Status-Code: 0$CR\$" + assertGrep --matches=1 --ignore-case http.header "^Serval-Rhizome-Result-Bundle-Status-Message: .*bundle new to store.*$CR\$" + assertGrep --matches=1 --ignore-case http.header "^Serval-Rhizome-Result-Payload-Status-Code: 1$CR\$" + assertGrep --matches=1 --ignore-case http.header "^Serval-Rhizome-Result-Payload-Status-Message: .*payload new to store.*$CR\$" extract_manifest_id BID empty.manifest executeOk_servald rhizome list assert_rhizome_list empty @@ -504,9 +674,13 @@ test_RhizomeInsertLarge() { --form "manifest=;type=rhizome-manifest/text" \ --form "payload=@file1" \ "http://$addr_localhost:$PORTA/restful/rhizome/insert" - tfw_cat http.header file1.manifest + tfw_cat http.header -v file1.manifest assertExitStatus == 0 assertStdoutIs 201 + assertGrep --matches=1 --ignore-case http.header "^Serval-Rhizome-Result-Bundle-Status-Code: 0$CR\$" + assertGrep --matches=1 --ignore-case http.header "^Serval-Rhizome-Result-Bundle-Status-Message: .*bundle new to store.*$CR\$" + assertGrep --matches=1 --ignore-case http.header "^Serval-Rhizome-Result-Payload-Status-Code: 1$CR\$" + assertGrep --matches=1 --ignore-case http.header "^Serval-Rhizome-Result-Payload-Status-Message: .*payload new to store.*$CR\$" extract_manifest_id BID file1.manifest executeOk_servald rhizome list assert_rhizome_list file1 @@ -730,7 +904,8 @@ test_RhizomeInsertIncorrectFilesize() { assertExitStatus == 0 assertStdoutIs 403 assertJq http.body 'contains({"http_status_code": 403})' - assertJqGrep --ignore-case http.body '.http_status_message' 'payload size.*contradicts manifest' + assertJq http.body 'contains({"rhizome_payload_status_code": 3})' + assertJqGrep --ignore-case http.body '.rhizome_payload_status_message' 'payload size.*contradicts manifest' execute curl \ --silent --show-error --write-out '%{http_code}' \ --output http.body \ @@ -742,7 +917,9 @@ test_RhizomeInsertIncorrectFilesize() { tfw_cat http.header http.body assertExitStatus == 0 assertStdoutIs 403 - assertGrep --ignore-case http.body 'payload size.*contradicts manifest' + assertJq http.body 'contains({"http_status_code": 403})' + assertJq http.body 'contains({"rhizome_payload_status_code": 3})' + assertJqGrep --ignore-case http.body '.rhizome_payload_status_message' 'payload size.*contradicts manifest' executeOk_servald rhizome list assert_rhizome_list } @@ -766,7 +943,8 @@ test_RhizomeInsertIncorrectFilehash() { assertExitStatus == 0 assertStdoutIs 403 assertJq http.body 'contains({"http_status_code": 403})' - assertJqGrep --ignore-case http.body '.http_status_message' 'payload hash.*contradicts manifest' + assertJq http.body 'contains({"rhizome_payload_status_code": 4})' + assertJqGrep --ignore-case http.body '.rhizome_payload_status_message' 'payload hash.*contradicts manifest' executeOk_servald rhizome list assert_rhizome_list } From d16be8f42d75fe4715ed6197927a01fcde2f39dc Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Fri, 4 Jul 2014 12:08:49 +0930 Subject: [PATCH 12/21] Improve MeshMS HTTP RESTful interface Provide "meshms_status_message" in returned JSON status content --- .../servaldna/meshms/MeshMSCommon.java | 32 +++++++++++-------- meshms.c | 12 +++++++ meshms.h | 2 ++ meshms_restful.c | 31 +++++++++--------- tests/meshmsrestful | 6 ++-- 5 files changed, 52 insertions(+), 31 deletions(-) diff --git a/java/org/servalproject/servaldna/meshms/MeshMSCommon.java b/java/org/servalproject/servaldna/meshms/MeshMSCommon.java index b6858d5b..ee133f02 100644 --- a/java/org/servalproject/servaldna/meshms/MeshMSCommon.java +++ b/java/org/servalproject/servaldna/meshms/MeshMSCommon.java @@ -49,7 +49,7 @@ public class MeshMSCommon JSONTokeniser json = new JSONTokeniser(new InputStreamReader(conn.getErrorStream(), "US-ASCII")); Status status = decodeRestfulStatus(json); throwRestfulResponseExceptions(status, conn.getURL()); - throw new ServalDInterfaceException("unexpected MeshMS status = " + status.meshms_status + ", \"" + status.message + "\""); + throw new ServalDInterfaceException("unexpected MeshMS status = " + status.meshms_status_code + ", \"" + status.meshms_status_message + "\""); } for (int code: expected_response_codes) { if (conn.getResponseCode() == code) { @@ -61,8 +61,10 @@ public class MeshMSCommon } private static class Status { - public MeshMSStatus meshms_status; - public String message; + public int http_status_code; + public String http_status_message; + public MeshMSStatus meshms_status_code; + public String meshms_status_message; } protected static Status decodeRestfulStatus(JSONTokeniser json) throws IOException, ServalDInterfaceException @@ -72,15 +74,19 @@ public class MeshMSCommon json.consume(JSONTokeniser.Token.START_OBJECT); json.consume("http_status_code"); json.consume(JSONTokeniser.Token.COLON); - json.consume(Integer.class); + status.http_status_code = json.consume(Integer.class); json.consume(JSONTokeniser.Token.COMMA); - status.message = json.consume("http_status_message"); + json.consume("http_status_message"); json.consume(JSONTokeniser.Token.COLON); - String message = json.consume(String.class); + status.http_status_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)); + status.meshms_status_code = MeshMSStatus.fromCode(json.consume(Integer.class)); + json.consume(JSONTokeniser.Token.COMMA); + json.consume("meshms_status_message"); + json.consume(JSONTokeniser.Token.COLON); + status.meshms_status_message = json.consume(String.class); json.consume(JSONTokeniser.Token.END_OBJECT); json.consume(JSONTokeniser.Token.EOF); return status; @@ -92,7 +98,7 @@ public class MeshMSCommon protected static void throwRestfulResponseExceptions(Status status, URL url) throws MeshMSException, ServalDFailureException { - switch (status.meshms_status) { + switch (status.meshms_status_code) { case OK: case UPDATED: break; @@ -101,7 +107,7 @@ public class MeshMSCommon case PROTOCOL_FAULT: throw new MeshMSProtocolFaultException(url); case ERROR: - throw new ServalDFailureException("received meshms_status=ERROR(-1) from " + url); + throw new ServalDFailureException("received meshms_status_code=ERROR(-1) from " + url); } } @@ -125,7 +131,7 @@ public class MeshMSCommon JSONTokeniser json = MeshMSCommon.receiveRestfulResponse(conn, HttpURLConnection.HTTP_CREATED); Status status = decodeRestfulStatus(json); throwRestfulResponseExceptions(status, conn.getURL()); - return status.meshms_status; + return status.meshms_status_code; } public static MeshMSStatus markAllConversationsRead(ServalDHttpConnectionFactory connector, SubscriberId sid1) throws IOException, ServalDInterfaceException, MeshMSException @@ -137,7 +143,7 @@ public class MeshMSCommon JSONTokeniser json = MeshMSCommon.receiveRestfulResponse(conn, expected_response_codes); Status status = decodeRestfulStatus(json); throwRestfulResponseExceptions(status, conn.getURL()); - return status.meshms_status; + return status.meshms_status_code; } public static MeshMSStatus markAllMessagesRead(ServalDHttpConnectionFactory connector, SubscriberId sid1, SubscriberId sid2) throws IOException, ServalDInterfaceException, MeshMSException @@ -149,7 +155,7 @@ public class MeshMSCommon JSONTokeniser json = MeshMSCommon.receiveRestfulResponse(conn, expected_response_codes); Status status = decodeRestfulStatus(json); throwRestfulResponseExceptions(status, conn.getURL()); - return status.meshms_status; + return status.meshms_status_code; } public static MeshMSStatus advanceReadOffset(ServalDHttpConnectionFactory connector, SubscriberId sid1, SubscriberId sid2, long offset) throws IOException, ServalDInterfaceException, MeshMSException @@ -161,7 +167,7 @@ public class MeshMSCommon JSONTokeniser json = MeshMSCommon.receiveRestfulResponse(conn, expected_response_codes); Status status = decodeRestfulStatus(json); throwRestfulResponseExceptions(status, conn.getURL()); - return status.meshms_status; + return status.meshms_status_code; } } diff --git a/meshms.c b/meshms.c index 28ce99e1..7860aa3b 100644 --- a/meshms.c +++ b/meshms.c @@ -1244,3 +1244,15 @@ done: keyring = NULL; return ret; } + +const char *meshms_status_message(enum meshms_status status) +{ + switch (status) { + case MESHMS_STATUS_OK: return "OK"; + case MESHMS_STATUS_UPDATED: return "Updated"; + case MESHMS_STATUS_SID_LOCKED: return "Identity unknown"; + case MESHMS_STATUS_PROTOCOL_FAULT: return "MeshMS protocol fault"; + case MESHMS_STATUS_ERROR: return "Internal error"; + } + return NULL; +} diff --git a/meshms.h b/meshms.h index 96663a4f..9ee202e0 100644 --- a/meshms.h +++ b/meshms.h @@ -47,6 +47,8 @@ __MESHMS_INLINE int meshms_failed(enum meshms_status status) { return status != MESHMS_STATUS_OK && status != MESHMS_STATUS_UPDATED; } +const char *meshms_status_message(enum meshms_status); + // the manifest details for one half of a conversation struct meshms_ply { rhizome_bid_t bundle_id; diff --git a/meshms_restful.c b/meshms_restful.c index 308cf01c..359f0b54 100644 --- a/meshms_restful.c +++ b/meshms_restful.c @@ -66,42 +66,41 @@ static int strn_to_meshms_token(const char *str, rhizome_bid_t *bidp, uint64_t * static int http_request_meshms_response(struct httpd_request *r, uint16_t result, const char *message, enum meshms_status status) { - r->http.response.result_extra[0].label = "meshms_status_code"; - r->http.response.result_extra[0].value.type = JSON_INTEGER; - r->http.response.result_extra[0].value.u.integer = status; uint16_t meshms_result = 0; - const char *meshms_message = NULL; switch (status) { case MESHMS_STATUS_OK: meshms_result = 200; - meshms_message = "OK"; break; case MESHMS_STATUS_UPDATED: meshms_result = 201; - meshms_message = "Updated"; break; case MESHMS_STATUS_SID_LOCKED: - meshms_result = 403; - meshms_message = "Identity unknown"; - break; case MESHMS_STATUS_PROTOCOL_FAULT: meshms_result = 403; - meshms_message = "MeshMS protocol fault"; break; case MESHMS_STATUS_ERROR: meshms_result = 500; break; } - if (!meshms_result) { + if (meshms_result == 0) { WHYF("Invalid MeshMS status code %d", status); - result = 500; - } else if (!result) { + meshms_result = 500; + } + r->http.response.result_extra[0].label = "meshms_status_code"; + r->http.response.result_extra[0].value.type = JSON_INTEGER; + r->http.response.result_extra[0].value.u.integer = status; + const char *status_message = meshms_status_message(status); + if (status_message) { + r->http.response.result_extra[1].label = "meshms_status_message"; + r->http.response.result_extra[1].value.type = JSON_STRING_NULTERM; + r->http.response.result_extra[1].value.u.string.content = status_message; + } + if (meshms_result > result) { result = meshms_result; - if (message == NULL) - message = meshms_message; + message = NULL; } assert(result != 0); - http_request_simple_response(&r->http, result, message); + http_request_simple_response(&r->http, result, message ? message : result == 403 ? "MeshMS operation failed" : NULL); return result; } diff --git a/tests/meshmsrestful b/tests/meshmsrestful index 128b3442..78b9d508 100755 --- a/tests/meshmsrestful +++ b/tests/meshmsrestful @@ -280,8 +280,9 @@ test_MeshmsListMessagesNoIdentity() { assertExitStatus == 0 assertStdoutIs 403 assertJq http.body 'contains({"http_status_code": 403})' + assertJqGrep --ignore-case http.body '.http_status_message' 'meshms operation failed' assertJq http.body 'contains({"meshms_status_code": 2})' - assertJqGrep --ignore-case http.body '.http_status_message' 'identity.*unknown' + assertJqGrep --ignore-case http.body '.meshms_status_message' 'identity.*unknown' } doc_MeshmsListMessagesNewSince="HTTP RESTful list MeshMS messages in one conversation since token as JSON" @@ -555,8 +556,9 @@ test_MeshmsSendNoIdentity() { assertExitStatus == 0 assertStdoutIs 403 assertJq http.body 'contains({"http_status_code": 403})' + assertJqGrep --ignore-case http.body '.http_status_message' 'meshms operation failed' assertJq http.body 'contains({"meshms_status_code": 2})' - assertJqGrep --ignore-case http.body '.http_status_message' 'identity.*unknown' + assertJqGrep --ignore-case http.body '.meshms_status_message' 'identity.*unknown' } doc_MeshmsReadAllConversations="HTTP RESTful MeshMS mark all conversations read" From 2aec8f31a4ea71196d6041a3a59421884caaa0f0 Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Thu, 3 Jul 2014 17:23:11 +0930 Subject: [PATCH 13/21] Rhizome Java API: get decrypted payload --- .../org/servalproject/json/JSONTokeniser.java | 13 ++ .../servaldna/ServalDClient.java | 13 +- .../servaldna/rhizome/RhizomeBundleList.java | 2 +- .../rhizome/RhizomeBundleStatus.java | 71 +++++++++++ .../servaldna/rhizome/RhizomeCommon.java | 113 +++++++++++++++--- .../rhizome/RhizomeDecryptionException.java | 37 ++++++ .../servaldna/rhizome/RhizomeException.java | 40 +++++++ .../rhizome/RhizomeFakeManifestException.java | 36 ++++++ .../RhizomeInconsistencyException.java | 37 ++++++ .../RhizomeInvalidManifestException.java | 38 ++++++ .../rhizome/RhizomePayloadBundle.java | 50 ++++++++ .../rhizome/RhizomePayloadStatus.java | 71 +++++++++++ java/org/servalproject/test/Rhizome.java | 68 +++++++++-- rhizome_restful.c | 4 +- tests/rhizomejava | 36 ++++++ 15 files changed, 597 insertions(+), 32 deletions(-) create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeBundleStatus.java create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeDecryptionException.java create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeException.java create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeFakeManifestException.java create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeInconsistencyException.java create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeInvalidManifestException.java create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomePayloadBundle.java create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomePayloadStatus.java diff --git a/java/org/servalproject/json/JSONTokeniser.java b/java/org/servalproject/json/JSONTokeniser.java index 9ea14c7a..5c5da1b5 100644 --- a/java/org/servalproject/json/JSONTokeniser.java +++ b/java/org/servalproject/json/JSONTokeniser.java @@ -54,6 +54,10 @@ public class JSONTokeniser { public static class UnexpectedException extends JSONInputException { + public UnexpectedException(String got) { + super("unexpected " + got); + } + public UnexpectedException(String got, Class expecting) { super("unexpected " + got + ", expecting " + expecting.getName()); } @@ -78,6 +82,10 @@ public class JSONTokeniser { public static class UnexpectedTokenException extends UnexpectedException { + public UnexpectedTokenException(Object got) { + super(jsonTokenDescription(got)); + } + public UnexpectedTokenException(Object got, Class expecting) { super(jsonTokenDescription(got), expecting); } @@ -115,6 +123,11 @@ public class JSONTokeniser { return n; } + public static void unexpected(Object tok) throws UnexpectedTokenException + { + throw new UnexpectedTokenException(tok); + } + public static void match(Object tok, Token exactly) throws SyntaxException { if (tok != exactly) diff --git a/java/org/servalproject/servaldna/ServalDClient.java b/java/org/servalproject/servaldna/ServalDClient.java index 780e1f68..afec9e39 100644 --- a/java/org/servalproject/servaldna/ServalDClient.java +++ b/java/org/servalproject/servaldna/ServalDClient.java @@ -39,6 +39,8 @@ import org.servalproject.servaldna.rhizome.RhizomeCommon; import org.servalproject.servaldna.rhizome.RhizomeBundleList; import org.servalproject.servaldna.rhizome.RhizomeManifestBundle; import org.servalproject.servaldna.rhizome.RhizomePayloadRawBundle; +import org.servalproject.servaldna.rhizome.RhizomePayloadBundle; +import org.servalproject.servaldna.rhizome.RhizomeException; import org.servalproject.servaldna.meshms.MeshMSCommon; import org.servalproject.servaldna.meshms.MeshMSConversationList; import org.servalproject.servaldna.meshms.MeshMSMessageList; @@ -63,23 +65,28 @@ public class ServalDClient implements ServalDHttpConnectionFactory this.restfulPassword = restfulPassword; } - public RhizomeBundleList rhizomeListBundles() throws ServalDInterfaceException, IOException + public RhizomeBundleList rhizomeListBundles() throws ServalDInterfaceException, IOException, RhizomeException { RhizomeBundleList list = new RhizomeBundleList(this); list.connect(); return list; } - public RhizomeManifestBundle rhizomeManifest(BundleId bid) throws ServalDInterfaceException, IOException + public RhizomeManifestBundle rhizomeManifest(BundleId bid) throws ServalDInterfaceException, IOException, RhizomeException { return RhizomeCommon.rhizomeManifest(this, bid); } - public RhizomePayloadRawBundle rhizomePayloadRaw(BundleId bid) throws ServalDInterfaceException, IOException + public RhizomePayloadRawBundle rhizomePayloadRaw(BundleId bid) throws ServalDInterfaceException, IOException, RhizomeException { return RhizomeCommon.rhizomePayloadRaw(this, bid); } + public RhizomePayloadBundle rhizomePayload(BundleId bid) throws ServalDInterfaceException, IOException, RhizomeException + { + return RhizomeCommon.rhizomePayload(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/RhizomeBundleList.java b/java/org/servalproject/servaldna/rhizome/RhizomeBundleList.java index 47845bca..d1e1d6ff 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeBundleList.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeBundleList.java @@ -68,7 +68,7 @@ public class RhizomeBundleList { return this.json != null; } - public void connect() throws IOException, ServalDInterfaceException + public void connect() throws IOException, ServalDInterfaceException, RhizomeException { try { rowCount = 0; diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeBundleStatus.java b/java/org/servalproject/servaldna/rhizome/RhizomeBundleStatus.java new file mode 100644 index 00000000..92d817a4 --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeBundleStatus.java @@ -0,0 +1,71 @@ +/** + * 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.ServalDInterfaceException; + +/* This enum is a direct isomorphism from the C "enum rhizome_bundle_status" defined in rhizome.h. + */ +public enum RhizomeBundleStatus { + ERROR(-1), // internal error + NEW(0), // bundle is newer than store + SAME(1), // same version already in store + DUPLICATE(2), // equivalent bundle already in store + OLD(3), // newer version already in store + INVALID(4), // manifest is invalid + FAKE(5), // manifest signature not valid + INCONSISTENT(6), // manifest filesize/filehash does not match supplied payload + NO_ROOM(7) // doesn't fit; store may contain more important bundles + ; + + final public int code; + + private RhizomeBundleStatus(int code) { + this.code = code; + } + + public static RhizomeBundleStatus fromCode(int code) throws InvalidException + { + RhizomeBundleStatus status = null; + switch (code) { + case -1: status = ERROR; break; + case 0: status = NEW; break; + case 1: status = SAME; break; + case 2: status = DUPLICATE; break; + case 3: status = OLD; break; + case 4: status = INVALID; break; + case 5: status = FAKE; break; + case 6: status = INCONSISTENT; break; + case 7: status = NO_ROOM; break; + default: throw new InvalidException(code); + } + assert status.code == code; + return status; + } + + public static class InvalidException extends ServalDInterfaceException + { + public InvalidException(int code) { + super("invalid Rhizome bundle status code = " + code); + } + } + +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java index 30eb9279..6271434b 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java @@ -28,6 +28,7 @@ import java.io.IOException; import java.io.PrintStream; import java.io.InputStream; import java.io.InputStreamReader; +import java.net.URL; import java.net.HttpURLConnection; import org.servalproject.json.JSONTokeniser; import org.servalproject.json.JSONInputException; @@ -43,13 +44,22 @@ import org.servalproject.servaldna.ServalDFailureException; public class RhizomeCommon { - protected static InputStream receiveResponse(HttpURLConnection conn, int expected_response_code) throws IOException, ServalDInterfaceException + private static class Status { + public int http_status_code; + public String http_status_message; + RhizomeBundleStatus bundle_status_code; + String bundle_status_message; + RhizomePayloadStatus payload_status_code; + String payload_status_message; + } + + protected static InputStream receiveResponse(HttpURLConnection conn, int expected_response_code) throws IOException, ServalDInterfaceException, RhizomeException { 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 + protected static InputStream receiveResponse(HttpURLConnection conn, int[] expected_response_codes) throws IOException, ServalDInterfaceException, RhizomeException { for (int code: expected_response_codes) { if (conn.getResponseCode() == code) @@ -60,18 +70,64 @@ public class RhizomeCommon 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 + "\""); + throwRestfulResponseExceptions(status, conn.getURL()); + throw new ServalDInterfaceException( + "unexpected Rhizome failure, \"" + status.http_status_message + "\"" + + (status.bundle_status_code == null ? "" : ", " + status.bundle_status_code) + + (status.bundle_status_message == null ? "" : " \"" + status.bundle_status_message + "\"") + + (status.payload_status_code == null ? "" : ", " + status.payload_status_code) + + (status.payload_status_message == null ? "" : ", " + status.payload_status_message + "\"") + ); } throw new ServalDInterfaceException("unexpected HTTP response code: " + conn.getResponseCode()); } - protected static JSONTokeniser receiveRestfulResponse(HttpURLConnection conn, int expected_response_code) throws IOException, ServalDInterfaceException + protected static void throwRestfulResponseExceptions(Status status, URL url) throws RhizomeException, ServalDFailureException + { + if (status.bundle_status_code != null) { + switch (status.bundle_status_code) { + case ERROR: + throw new ServalDFailureException("received rhizome_bundle_status_code=ERROR(-1) from " + url); + case NEW: + case SAME: + case DUPLICATE: + case OLD: + case NO_ROOM: + break; + case INVALID: + throw new RhizomeInvalidManifestException(url); + case FAKE: + throw new RhizomeFakeManifestException(url); + case INCONSISTENT: + throw new RhizomeInconsistencyException(url); + } + } + if (status.payload_status_code != null) { + switch (status.payload_status_code) { + case ERROR: + throw new ServalDFailureException("received rhizome_payload_status_code=ERROR(-1) from " + url); + case EMPTY: + case NEW: + case STORED: + case TOO_BIG: + case EVICTED: + break; + case WRONG_SIZE: + case WRONG_HASH: + throw new RhizomeInconsistencyException(url); + case CRYPTO_FAIL: + throw new RhizomeDecryptionException(url); + } + } + } + + protected static JSONTokeniser receiveRestfulResponse(HttpURLConnection conn, int expected_response_code) throws IOException, ServalDInterfaceException, RhizomeException { int[] expected_response_codes = { expected_response_code }; return receiveRestfulResponse(conn, expected_response_codes); } - protected static JSONTokeniser receiveRestfulResponse(HttpURLConnection conn, int[] expected_response_codes) throws IOException, ServalDInterfaceException + protected static JSONTokeniser receiveRestfulResponse(HttpURLConnection conn, int[] expected_response_codes) throws IOException, ServalDInterfaceException, RhizomeException { InputStream in = receiveResponse(conn, expected_response_codes); if (!conn.getContentType().equals("application/json")) @@ -79,10 +135,6 @@ public class RhizomeCommon return new JSONTokeniser(new InputStreamReader(in, "US-ASCII")); } - private static class Status { - public String message; - } - protected static Status decodeRestfulStatus(JSONTokeniser json) throws IOException, ServalDInterfaceException { try { @@ -90,12 +142,28 @@ public class RhizomeCommon json.consume(JSONTokeniser.Token.START_OBJECT); json.consume("http_status_code"); json.consume(JSONTokeniser.Token.COLON); - json.consume(Integer.class); + status.http_status_code = json.consume(Integer.class); json.consume(JSONTokeniser.Token.COMMA); - status.message = json.consume("http_status_message"); + json.consume("http_status_message"); json.consume(JSONTokeniser.Token.COLON); - String message = json.consume(String.class); - json.consume(JSONTokeniser.Token.END_OBJECT); + status.http_status_message = json.consume(String.class); + Object tok = json.nextToken(); + while (tok == JSONTokeniser.Token.COMMA) { + String label = json.consume(String.class); + json.consume(JSONTokeniser.Token.COLON); + if (label.equals("rhizome_bundle_status_code")) + status.bundle_status_code = RhizomeBundleStatus.fromCode(json.consume(Integer.class)); + else if (label.equals("rhizome_bundle_status_message")) + status.bundle_status_message = json.consume(String.class); + else if (label.equals("rhizome_payload_status_code")) + status.payload_status_code = RhizomePayloadStatus.fromCode(json.consume(Integer.class)); + else if (label.equals("rhizome_payload_status_message")) + status.payload_status_message = json.consume(String.class); + else + json.unexpected(label); + tok = json.nextToken(); + } + json.match(tok, JSONTokeniser.Token.END_OBJECT); json.consume(JSONTokeniser.Token.EOF); return status; } @@ -104,7 +172,7 @@ public class RhizomeCommon } } - public static RhizomeManifestBundle rhizomeManifest(ServalDHttpConnectionFactory connector, BundleId bid) throws IOException, ServalDInterfaceException + public static RhizomeManifestBundle rhizomeManifest(ServalDHttpConnectionFactory connector, BundleId bid) throws IOException, ServalDInterfaceException, RhizomeException { HttpURLConnection conn = connector.newServalDHttpConnection("/restful/rhizome/" + bid.toHex() + ".rhm"); conn.connect(); @@ -128,7 +196,7 @@ public class RhizomeCommon return new RhizomeManifestBundle(manifest, insertTime, author, secret); } - public static RhizomePayloadRawBundle rhizomePayloadRaw(ServalDHttpConnectionFactory connector, BundleId bid) throws IOException, ServalDInterfaceException + public static RhizomePayloadRawBundle rhizomePayloadRaw(ServalDHttpConnectionFactory connector, BundleId bid) throws IOException, ServalDInterfaceException, RhizomeException { HttpURLConnection conn = connector.newServalDHttpConnection("/restful/rhizome/" + bid.toHex() + "/raw.bin"); conn.connect(); @@ -143,6 +211,21 @@ public class RhizomeCommon return new RhizomePayloadRawBundle(manifest, in, insertTime, author, secret); } + public static RhizomePayloadBundle rhizomePayload(ServalDHttpConnectionFactory connector, BundleId bid) throws IOException, ServalDInterfaceException, RhizomeException + { + HttpURLConnection conn = connector.newServalDHttpConnection("/restful/rhizome/" + bid.toHex() + "/decrypted.bin"); + conn.connect(); + InputStream in = RhizomeCommon.receiveResponse(conn, HttpURLConnection.HTTP_OK); + if (!conn.getContentType().equals("application/octet-stream")) + throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType()); + dumpHeaders(conn, System.err); + RhizomeManifest manifest = manifestFromHeaders(conn); + 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 RhizomePayloadBundle(manifest, in, insertTime, author, secret); + } + private static void dumpHeaders(HttpURLConnection conn, PrintStream out) { for (Map.Entry> e: conn.getHeaderFields().entrySet()) diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeDecryptionException.java b/java/org/servalproject/servaldna/rhizome/RhizomeDecryptionException.java new file mode 100644 index 00000000..2d44d719 --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeDecryptionException.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.rhizome; + +import java.net.URL; + +/** + * Thrown when a Rhizome API method is asked to decrypt a payload without possessing the necessary + * recipient identity (ie, is locked or not in the keyring). + * + * @author Andrew Bettison + */ +public class RhizomeDecryptionException extends RhizomeException +{ + public RhizomeDecryptionException(URL url) { + super("cannot decrypt payload", url); + } + +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeException.java b/java/org/servalproject/servaldna/rhizome/RhizomeException.java new file mode 100644 index 00000000..2b895791 --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeException.java @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.rhizome; + +import java.net.URL; + +/** + * Thrown when a Rhizome API encounters an exceptional condition. This exception is subclassed for + * specific causes. + * + * @author Andrew Bettison + */ +public abstract class RhizomeException extends Exception +{ + public final URL url; + + public RhizomeException(String message, URL url) { + super(message + "; " + url); + this.url = url; + } + +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeFakeManifestException.java b/java/org/servalproject/servaldna/rhizome/RhizomeFakeManifestException.java new file mode 100644 index 00000000..7bf44c67 --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeFakeManifestException.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.rhizome; + +import java.net.URL; + +/** + * Thrown when a Rhizome API method is passed a manifest with an invalid or missing signature. + * + * @author Andrew Bettison + */ +public class RhizomeFakeManifestException extends RhizomeException +{ + public RhizomeFakeManifestException(URL url) { + super("unsigned manifest", url); + } + +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeInconsistencyException.java b/java/org/servalproject/servaldna/rhizome/RhizomeInconsistencyException.java new file mode 100644 index 00000000..cdab7985 --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeInconsistencyException.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.rhizome; + +import java.net.URL; + +/** + * Thrown when a Rhizome API method is passed a manifest which is inconsistent with a supplied + * payload. I.e., filesize or filehash does not match. + * + * @author Andrew Bettison + */ +public class RhizomeInconsistencyException extends RhizomeException +{ + public RhizomeInconsistencyException(URL url) { + super("manifest inconsistent with payload", url); + } + +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeInvalidManifestException.java b/java/org/servalproject/servaldna/rhizome/RhizomeInvalidManifestException.java new file mode 100644 index 00000000..f76bc01b --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeInvalidManifestException.java @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.rhizome; + +import java.net.URL; + +/** + * Thrown when a Rhizome API method is passed an invalid manifest. This is not an error within the + * Serval DNA interface, so it is not a subclass of ServalDInterfaceException. The programmer must + * explicitly deal with it instead of just absorbing it as an interface malfunction. + * + * @author Andrew Bettison + */ +public class RhizomeInvalidManifestException extends RhizomeException +{ + public RhizomeInvalidManifestException(URL url) { + super("invalid manifest", url); + } + +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomePayloadBundle.java b/java/org/servalproject/servaldna/rhizome/RhizomePayloadBundle.java new file mode 100644 index 00000000..590eea0c --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomePayloadBundle.java @@ -0,0 +1,50 @@ +/** + * 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.InputStream; +import org.servalproject.servaldna.SubscriberId; +import org.servalproject.servaldna.BundleSecret; +import org.servalproject.servaldna.ServalDInterfaceException; + +public class RhizomePayloadBundle { + + public final RhizomeManifest manifest; + public final InputStream payloadInputStream; + public final long insertTime; + public final SubscriberId author; + public final BundleSecret secret; + + protected RhizomePayloadBundle(RhizomeManifest manifest, + InputStream payloadInputStream, + long insertTime, + SubscriberId author, + BundleSecret secret) + + { + this.payloadInputStream = payloadInputStream; + this.manifest = manifest; + this.insertTime = insertTime; + this.author = author; + this.secret = secret; + } + +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomePayloadStatus.java b/java/org/servalproject/servaldna/rhizome/RhizomePayloadStatus.java new file mode 100644 index 00000000..9828aa73 --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomePayloadStatus.java @@ -0,0 +1,71 @@ +/** + * 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.ServalDInterfaceException; + +/* This enum is a direct isomorphism from the C "enum rhizome_payload_status" defined in rhizome.h. + */ +public enum RhizomePayloadStatus { + ERROR(-1), // unexpected error (underlying failure) + EMPTY(0), // payload is empty (zero length) + NEW(1), // payload is not yet in store (added) + STORED(2), // payload is already in store + WRONG_SIZE(3), // payload's size does not match manifest + WRONG_HASH(4), // payload's hash does not match manifest + CRYPTO_FAIL(5), // cannot encrypt/decrypt (payload key unknown) + TOO_BIG(6), // payload will never fit in our store + EVICTED(7) // other payloads in our store are more important + ; + + final public int code; + + private RhizomePayloadStatus(int code) { + this.code = code; + } + + public static RhizomePayloadStatus fromCode(int code) throws InvalidException + { + RhizomePayloadStatus status = null; + switch (code) { + case -1: status = ERROR; break; + case 0: status = EMPTY; break; + case 1: status = NEW; break; + case 2: status = STORED; break; + case 3: status = WRONG_SIZE; break; + case 4: status = WRONG_HASH; break; + case 5: status = CRYPTO_FAIL; break; + case 6: status = TOO_BIG; break; + case 7: status = EVICTED; break; + default: throw new InvalidException(code); + } + assert status.code == code; + return status; + } + + public static class InvalidException extends ServalDInterfaceException + { + public InvalidException(int code) { + super("invalid Rhizome payload status code = " + code); + } + } + +} diff --git a/java/org/servalproject/test/Rhizome.java b/java/org/servalproject/test/Rhizome.java index 0e572191..d66c62c4 100644 --- a/java/org/servalproject/test/Rhizome.java +++ b/java/org/servalproject/test/Rhizome.java @@ -33,6 +33,8 @@ import org.servalproject.servaldna.rhizome.RhizomeListBundle; import org.servalproject.servaldna.rhizome.RhizomeBundleList; import org.servalproject.servaldna.rhizome.RhizomeManifestBundle; import org.servalproject.servaldna.rhizome.RhizomePayloadRawBundle; +import org.servalproject.servaldna.rhizome.RhizomePayloadBundle; +import org.servalproject.servaldna.rhizome.RhizomeException; public class Rhizome { @@ -68,6 +70,9 @@ public class Rhizome { ); } } + catch (RhizomeException e) { + System.out.println(e.toString()); + } finally { if (list != null) list.close(); @@ -77,17 +82,22 @@ public class Rhizome { 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(); + try { + 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(); + } + catch (RhizomeException e) { + System.out.println(e.toString()); + } System.exit(0); } @@ -112,6 +122,40 @@ public class Rhizome { manifestFields(bundle.manifest, "\n") + "\n" ); } + catch (RhizomeException e) { + System.out.println(e.toString()); + } + finally { + if (out != null) + out.close(); + } + System.exit(0); + } + + static void rhizome_payload_decrypted(BundleId bid, String dstpath) throws ServalDInterfaceException, IOException, InterruptedException + { + ServalDClient client = new ServerControl().getRestfulClient(); + FileOutputStream out = new FileOutputStream(dstpath); + try { + RhizomePayloadBundle bundle = client.rhizomePayload(bid); + InputStream in = bundle.payloadInputStream; + byte[] buf = new byte[4096]; + int n; + while ((n = in.read(buf)) > 0) + out.write(buf, 0, n); + in.close(); + out.close(); + out = null; + System.out.println( + "_insertTime=" + bundle.insertTime + "\n" + + "_author=" + bundle.author + "\n" + + "_secret=" + bundle.secret + "\n" + + manifestFields(bundle.manifest, "\n") + "\n" + ); + } + catch (RhizomeException e) { + System.out.println(e.toString()); + } finally { if (out != null) out.close(); @@ -131,6 +175,8 @@ public class Rhizome { rhizome_manifest(new BundleId(args[1]), args[2]); else if (methodName.equals("rhizome-payload-raw")) rhizome_payload_raw(new BundleId(args[1]), args[2]); + else if (methodName.equals("rhizome-payload-decrypted")) + rhizome_payload_decrypted(new BundleId(args[1]), args[2]); } catch (Exception e) { e.printStackTrace(); System.exit(1); diff --git a/rhizome_restful.c b/rhizome_restful.c index 47a96ba7..c05fb285 100644 --- a/rhizome_restful.c +++ b/rhizome_restful.c @@ -727,7 +727,7 @@ static int restful_rhizome_bid_raw_bin(httpd_request *r, const char *remainder) } int ret = rhizome_response_content_init_filehash(r, &r->manifest->filehash); if (ret) - return ret; + return http_request_rhizome_response(r, ret, NULL, NULL); http_request_response_generated(&r->http, 200, CONTENT_TYPE_BLOB, rhizome_payload_content); return 1; } @@ -745,7 +745,7 @@ static int restful_rhizome_bid_decrypted_bin(httpd_request *r, const char *remai } int ret = rhizome_response_content_init_payload(r, r->manifest); if (ret) - return ret; + return http_request_rhizome_response(r, ret, NULL, NULL); // TODO use Content Type from manifest (once it is implemented) http_request_response_generated(&r->http, 200, CONTENT_TYPE_BLOB, rhizome_payload_content); return 1; diff --git a/tests/rhizomejava b/tests/rhizomejava index 0b199221..d1f698b8 100755 --- a/tests/rhizomejava +++ b/tests/rhizomejava @@ -180,8 +180,44 @@ test_RhizomePayloadRaw() { executeJavaOk org.servalproject.test.Rhizome rhizome-payload-raw "${BID[$n]}" raw.bin$n tfw_cat --stdout --stderr assert_metadata $n + done + for n in 0 1 2 3; do assert cmp raw$n raw.bin$n done } +doc_RhizomePayloadDecrypted="Java API fetch Rhizome decrypted payload" +setup_RhizomePayloadDecrypted() { + setup + rhizome_add_bundles $SIDA1 0 1 + rhizome_add_bundles --encrypted $SIDA1 2 3 +} +test_RhizomePayloadDecrypted() { + for n in 0 1 2 3; do + executeJavaOk org.servalproject.test.Rhizome rhizome-payload-decrypted "${BID[$n]}" decrypted.bin$n + tfw_cat --stdout --stderr + assert_metadata $n + done + for n in 0 1 2 3; do + assert cmp file$n decrypted.bin$n + done +} + +doc_RhizomePayloadDecryptedForeign="Java API cannot fetch foreign Rhizome decrypted payload" +setup_RhizomePayloadDecryptedForeign() { + setup + rhizome_add_bundles --encrypted $SIDA1 0 0 + set_instance +B + create_single_identity + rhizome_add_bundles --encrypted $SIDB 1 1 + executeOk_servald rhizome export manifest "${BID[1]}" file1.manifest + set_instance +A + executeOk_servald rhizome import bundle raw1 file1.manifest +} +test_RhizomePayloadDecryptedForeign() { + executeJavaOk org.servalproject.test.Rhizome rhizome-payload-decrypted "${BID[1]}" decrypted.bin$n + tfw_cat --stdout --stderr + assertStdoutGrep RhizomeDecryptionException +} + runTests "$@" From 3715c5bf0b2be95bc773eb7e8225079cb55d7c81 Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Fri, 4 Jul 2014 17:48:40 +0930 Subject: [PATCH 14/21] Rhizome Java API: negative fetch tests --- http_server.c | 1 + .../servaldna/rhizome/RhizomeCommon.java | 320 +++++++++++++----- .../RhizomeDuplicateBundleException.java | 36 ++ ...RhizomeManifestAlreadyStoredException.java | 36 ++ .../RhizomeManifestNotFoundException.java | 36 ++ .../RhizomeOutdatedBundleException.java | 37 ++ .../rhizome/RhizomeStoreFullException.java | 36 ++ java/org/servalproject/test/Rhizome.java | 100 +++--- rhizome_restful.c | 6 +- tests/rhizomejava | 55 +++ tests/rhizomerestful | 14 +- 11 files changed, 548 insertions(+), 129 deletions(-) create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeDuplicateBundleException.java create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeManifestAlreadyStoredException.java create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeManifestNotFoundException.java create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeOutdatedBundleException.java create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeStoreFullException.java diff --git a/http_server.c b/http_server.c index 28f0c921..288dc716 100644 --- a/http_server.c +++ b/http_server.c @@ -1814,6 +1814,7 @@ static const char *httpResultString(int response_code) switch (response_code) { case 200: return "OK"; case 201: return "Created"; + case 204: return "No Content"; case 206: return "Partial Content"; case 400: return "Bad Request"; case 401: return "Unauthorized"; diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java index 6271434b..30c81dc8 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java @@ -21,6 +21,9 @@ package org.servalproject.servaldna.rhizome; import java.lang.StringBuilder; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.lang.reflect.InvocationTargetException; import java.util.Map; import java.util.List; @@ -45,6 +48,7 @@ public class RhizomeCommon { private static class Status { + InputStream input_stream; public int http_status_code; public String http_status_message; RhizomeBundleStatus bundle_status_code; @@ -53,35 +57,45 @@ public class RhizomeCommon String payload_status_message; } - protected static InputStream receiveResponse(HttpURLConnection conn, int expected_response_code) throws IOException, ServalDInterfaceException, RhizomeException + 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 InputStream receiveResponse(HttpURLConnection conn, int[] expected_response_codes) throws IOException, ServalDInterfaceException, RhizomeException + 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(); for (int code: expected_response_codes) { - if (conn.getResponseCode() == code) - return conn.getInputStream(); + if (status.http_status_code == code) { + status.input_stream = conn.getInputStream(); + return status; + } } 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); - throwRestfulResponseExceptions(status, conn.getURL()); - throw new ServalDInterfaceException( - "unexpected Rhizome failure, \"" + status.http_status_message + "\"" - + (status.bundle_status_code == null ? "" : ", " + status.bundle_status_code) - + (status.bundle_status_message == null ? "" : " \"" + status.bundle_status_message + "\"") - + (status.payload_status_code == null ? "" : ", " + status.payload_status_code) - + (status.payload_status_message == null ? "" : ", " + status.payload_status_message + "\"") - ); + decodeRestfulStatus(status, json); + return status; } throw new ServalDInterfaceException("unexpected HTTP response code: " + conn.getResponseCode()); } + protected static void unexpectedResponse(Status status) throws ServalDInterfaceException + { + throw new ServalDInterfaceException( + "unexpected Rhizome failure, \"" + status.http_status_message + "\"" + + (status.bundle_status_code == null ? "" : ", " + status.bundle_status_code) + + (status.bundle_status_message == null ? "" : " \"" + status.bundle_status_message + "\"") + + (status.payload_status_code == null ? "" : ", " + status.payload_status_code) + + (status.payload_status_message == null ? "" : ", " + status.payload_status_message + "\"") + ); + } + +/* protected static void throwRestfulResponseExceptions(Status status, URL url) throws RhizomeException, ServalDFailureException { if (status.bundle_status_code != null) { @@ -89,11 +103,15 @@ public class RhizomeCommon case ERROR: throw new ServalDFailureException("received rhizome_bundle_status_code=ERROR(-1) from " + url); case NEW: + throw new RhizomeManifestNotFoundException(url); case SAME: + throw new RhizomeManifestAlreadyStoredException(url); case DUPLICATE: + throw new RhizomeDuplicateBundleException(url); case OLD: + throw new RhizomeOutdatedBundleException(url); case NO_ROOM: - break; + throw new RhizomeStoreFullException(url); case INVALID: throw new RhizomeInvalidManifestException(url); case FAKE: @@ -102,24 +120,8 @@ public class RhizomeCommon throw new RhizomeInconsistencyException(url); } } - if (status.payload_status_code != null) { - switch (status.payload_status_code) { - case ERROR: - throw new ServalDFailureException("received rhizome_payload_status_code=ERROR(-1) from " + url); - case EMPTY: - case NEW: - case STORED: - case TOO_BIG: - case EVICTED: - break; - case WRONG_SIZE: - case WRONG_HASH: - throw new RhizomeInconsistencyException(url); - case CRYPTO_FAIL: - throw new RhizomeDecryptionException(url); - } - } } +*/ protected static JSONTokeniser receiveRestfulResponse(HttpURLConnection conn, int expected_response_code) throws IOException, ServalDInterfaceException, RhizomeException { @@ -129,21 +131,38 @@ public class RhizomeCommon protected static JSONTokeniser receiveRestfulResponse(HttpURLConnection conn, int[] expected_response_codes) throws IOException, ServalDInterfaceException, RhizomeException { - InputStream in = receiveResponse(conn, expected_response_codes); + Status status = receiveResponse(conn, expected_response_codes); if (!conn.getContentType().equals("application/json")) throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType()); - return new JSONTokeniser(new InputStreamReader(in, "US-ASCII")); + return new JSONTokeniser(new InputStreamReader(status.input_stream, "US-ASCII")); } - protected static Status decodeRestfulStatus(JSONTokeniser json) throws IOException, ServalDInterfaceException + protected static void decodeHeaderBundleStatus(Status status, HttpURLConnection conn) throws ServalDInterfaceException + { + status.bundle_status_code = header(conn, "Serval-Rhizome-Result-Bundle-Status-Code", RhizomeBundleStatus.class); + status.bundle_status_message = headerString(conn, "Serval-Rhizome-Result-Bundle-Status-Message"); + } + + protected static void decodeHeaderPayloadStatus(Status status, HttpURLConnection conn) throws ServalDInterfaceException + { + status.payload_status_code = header(conn, "Serval-Rhizome-Result-Payload-Status-Code", RhizomePayloadStatus.class); + status.payload_status_message = headerString(conn, "Serval-Rhizome-Result-Payload-Status-Message"); + } + + protected static void decodeRestfulStatus(Status status, 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); - status.http_status_code = json.consume(Integer.class); + 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); @@ -151,79 +170,185 @@ public class RhizomeCommon while (tok == JSONTokeniser.Token.COMMA) { String label = json.consume(String.class); json.consume(JSONTokeniser.Token.COLON); - if (label.equals("rhizome_bundle_status_code")) - status.bundle_status_code = RhizomeBundleStatus.fromCode(json.consume(Integer.class)); - else if (label.equals("rhizome_bundle_status_message")) - status.bundle_status_message = json.consume(String.class); - else if (label.equals("rhizome_payload_status_code")) - status.payload_status_code = RhizomePayloadStatus.fromCode(json.consume(Integer.class)); - else if (label.equals("rhizome_payload_status_message")) - status.payload_status_message = json.consume(String.class); + if (label.equals("rhizome_bundle_status_code")) { + RhizomeBundleStatus bs = RhizomeBundleStatus.fromCode(json.consume(Integer.class)); + if (status.bundle_status_code == null) + status.bundle_status_code = bs; + else if (status.bundle_status_code != bs) + throw new ServalDInterfaceException("JSON/header conflict" + + ", rhizome_bundle_status_code=" + bs.code + + " but Serval-Rhizome-Result-Bundle-Status-Code: " + status.bundle_status_code.code); + } + else if (label.equals("rhizome_bundle_status_message")) { + String message = json.consume(String.class); + if (status.bundle_status_message == null) + status.bundle_status_message = message; + else if (!status.bundle_status_message.equals(message)) + throw new ServalDInterfaceException("JSON/header conflict" + + ", rhizome_bundle_status_message=" + message + + " but Serval-Rhizome-Result-Bundle-Status-Message: " + status.bundle_status_message); + } + else if (label.equals("rhizome_payload_status_code")) { + RhizomePayloadStatus bs = RhizomePayloadStatus.fromCode(json.consume(Integer.class)); + if (status.payload_status_code == null) + status.payload_status_code = bs; + else if (status.payload_status_code != bs) + throw new ServalDInterfaceException("JSON/header conflict" + + ", rhizome_payload_status_code=" + bs.code + + " but Serval-Rhizome-Result-Payload-Status-Code: " + status.payload_status_code.code); + } + else if (label.equals("rhizome_payload_status_message")) { + String message = json.consume(String.class); + if (status.payload_status_message == null) + status.payload_status_message = message; + else if (!status.payload_status_message.equals(message)) + throw new ServalDInterfaceException("JSON/header conflict" + + ", rhizome_payload_status_message=" + message + + " but Serval-Rhizome-Result-Payload-Status-Code: " + status.payload_status_message); + } else json.unexpected(label); tok = json.nextToken(); } json.match(tok, JSONTokeniser.Token.END_OBJECT); json.consume(JSONTokeniser.Token.EOF); - return status; } catch (JSONInputException e) { throw new ServalDInterfaceException("malformed JSON status response", e); } } - public static RhizomeManifestBundle rhizomeManifest(ServalDHttpConnectionFactory connector, BundleId bid) throws IOException, ServalDInterfaceException, RhizomeException + 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; + Status status = RhizomeCommon.receiveResponse(conn, HttpURLConnection.HTTP_OK); try { - manifest = RhizomeManifest.fromTextFormat(in); + dumpHeaders(conn, System.err); + decodeHeaderBundleStatus(status, conn); + switch (status.bundle_status_code) { + case NEW: + return null; + case SAME: + if (!conn.getContentType().equals("rhizome-manifest/text")) + throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType()); + RhizomeManifest manifest = RhizomeManifest.fromTextFormat(status.input_stream); + BundleExtra extra = bundleExtraFromHeaders(conn); + return new RhizomeManifestBundle(manifest, extra.insertTime, extra.author, extra.secret); + case ERROR: + throw new ServalDFailureException("received rhizome_bundle_status_code=ERROR(-1) from " + conn.getURL()); + } } catch (RhizomeManifestParseException e) { throw new ServalDInterfaceException("malformed manifest from daemon", e); } finally { - in.close(); + if (status.input_stream != null) + status.input_stream.close(); } - dumpHeaders(conn, System.err); - 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); + unexpectedResponse(status); + return null; } - public static RhizomePayloadRawBundle rhizomePayloadRaw(ServalDHttpConnectionFactory connector, BundleId bid) throws IOException, ServalDInterfaceException, RhizomeException + public static RhizomePayloadRawBundle rhizomePayloadRaw(ServalDHttpConnectionFactory connector, BundleId bid) + throws IOException, ServalDInterfaceException { HttpURLConnection conn = connector.newServalDHttpConnection("/restful/rhizome/" + bid.toHex() + "/raw.bin"); conn.connect(); - InputStream in = RhizomeCommon.receiveResponse(conn, HttpURLConnection.HTTP_OK); - if (!conn.getContentType().equals("application/octet-stream")) - throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType()); - dumpHeaders(conn, System.err); - RhizomeManifest manifest = manifestFromHeaders(conn); - 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 RhizomePayloadRawBundle(manifest, in, insertTime, author, secret); + Status status = RhizomeCommon.receiveResponse(conn, HttpURLConnection.HTTP_OK); + try { + dumpHeaders(conn, System.err); + decodeHeaderBundleStatus(status, conn); + switch (status.bundle_status_code) { + case ERROR: + throw new ServalDFailureException("received rhizome_bundle_status_code=ERROR(-1) from " + conn.getURL()); + case NEW: // No manifest + return null; + case SAME: + decodeHeaderPayloadStatus(status, conn); + switch (status.payload_status_code) { + case ERROR: + throw new ServalDFailureException("received rhizome_payload_status_code=ERROR(-1) from " + conn.getURL()); + case NEW: + // The manifest is known but the payload is unavailable, so return a bundle + // object with a null input stream. + // FALL THROUGH + case EMPTY: + if (status.input_stream != null) { + status.input_stream.close(); + status.input_stream = null; + } + // FALL THROUGH + case STORED: { + if (status.input_stream != null && !conn.getContentType().equals("application/octet-stream")) + throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType()); + RhizomeManifest manifest = manifestFromHeaders(conn); + BundleExtra extra = bundleExtraFromHeaders(conn); + RhizomePayloadRawBundle ret = new RhizomePayloadRawBundle(manifest, status.input_stream, extra.insertTime, extra.author, extra.secret); + status.input_stream = null; // don't close when we return + return ret; + } + } + } + } + finally { + if (status.input_stream != null) + status.input_stream.close(); + } + unexpectedResponse(status); + return null; } - public static RhizomePayloadBundle rhizomePayload(ServalDHttpConnectionFactory connector, BundleId bid) throws IOException, ServalDInterfaceException, RhizomeException + public static RhizomePayloadBundle rhizomePayload(ServalDHttpConnectionFactory connector, BundleId bid) + throws IOException, ServalDInterfaceException, RhizomeDecryptionException { HttpURLConnection conn = connector.newServalDHttpConnection("/restful/rhizome/" + bid.toHex() + "/decrypted.bin"); conn.connect(); - InputStream in = RhizomeCommon.receiveResponse(conn, HttpURLConnection.HTTP_OK); - if (!conn.getContentType().equals("application/octet-stream")) - throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType()); - dumpHeaders(conn, System.err); - RhizomeManifest manifest = manifestFromHeaders(conn); - 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 RhizomePayloadBundle(manifest, in, insertTime, author, secret); + Status status = RhizomeCommon.receiveResponse(conn, HttpURLConnection.HTTP_OK); + try { + dumpHeaders(conn, System.err); + decodeHeaderBundleStatus(status, conn); + switch (status.bundle_status_code) { + case ERROR: + throw new ServalDFailureException("received rhizome_bundle_status_code=ERROR(-1) from " + conn.getURL()); + case NEW: // No manifest + return null; + case SAME: + decodeHeaderPayloadStatus(status, conn); + switch (status.payload_status_code) { + case ERROR: + throw new ServalDFailureException("received rhizome_payload_status_code=ERROR(-1) from " + conn.getURL()); + case CRYPTO_FAIL: + throw new RhizomeDecryptionException(conn.getURL()); + case NEW: + // The manifest is known but the payload is unavailable, so return a bundle + // object with a null input stream. + // FALL THROUGH + case EMPTY: + if (status.input_stream != null) { + status.input_stream.close(); + status.input_stream = null; + } + // FALL THROUGH + case STORED: { + if (status.input_stream != null && !conn.getContentType().equals("application/octet-stream")) + throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType()); + RhizomeManifest manifest = manifestFromHeaders(conn); + BundleExtra extra = bundleExtraFromHeaders(conn); + RhizomePayloadBundle ret = new RhizomePayloadBundle(manifest, status.input_stream, extra.insertTime, extra.author, extra.secret); + status.input_stream = null; // don't close when we return + return ret; + } + } + } + } + finally { + if (status.input_stream != null) + status.input_stream.close(); + } + unexpectedResponse(status); + return null; } private static void dumpHeaders(HttpURLConnection conn, PrintStream out) @@ -250,6 +375,21 @@ public class RhizomeCommon return new RhizomeManifest(id, version, filesize, filehash, sender, recipient, BK, crypt, tail, date, service, name); } + private static class BundleExtra { + public long insertTime; + public SubscriberId author; + public BundleSecret secret; + } + + private static BundleExtra bundleExtraFromHeaders(HttpURLConnection conn) throws ServalDInterfaceException + { + BundleExtra extra = new BundleExtra(); + extra.insertTime = headerUnsignedLong(conn, "Serval-Rhizome-Bundle-Inserttime"); + extra.author = header(conn, "Serval-Rhizome-Bundle-Author", SubscriberId.class); + extra.secret = header(conn, "Serval-Rhizome-Bundle-Secret", BundleSecret.class); + return extra; + } + private static String headerString(HttpURLConnection conn, String header) throws ServalDInterfaceException { String str = conn.getHeaderField(header); @@ -329,10 +469,30 @@ public class RhizomeCommon private static T headerOrNull(HttpURLConnection conn, String header, Class cls) throws ServalDInterfaceException { String str = conn.getHeaderField(header); - if (str == null) - return null; try { - return (T) cls.getConstructor(String.class).newInstance(str); + try { + Constructor constructor = cls.getConstructor(String.class); + if (str == null) + return null; + return constructor.newInstance(str); + } + catch (NoSuchMethodException e) { + } + try { + Method method = cls.getMethod("fromCode", Integer.TYPE); + if ((method.getModifiers() & Modifier.STATIC) != 0 && method.getReturnType() == cls) { + Integer integer = headerIntegerOrNull(conn, header); + if (integer == null) + return null; + return cls.cast(method.invoke(null, integer)); + } + } + catch (NoSuchMethodException e) { + } + throw new ServalDInterfaceException("don't know how to instantiate: " + cls.getName()); + } + catch (ServalDInterfaceException e) { + throw e; } catch (InvocationTargetException e) { throw new ServalDInterfaceException("invalid header field: " + header + ": " + str, e.getTargetException()); diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeDuplicateBundleException.java b/java/org/servalproject/servaldna/rhizome/RhizomeDuplicateBundleException.java new file mode 100644 index 00000000..bbc3ee4a --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeDuplicateBundleException.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.rhizome; + +import java.net.URL; + +/** + * Thrown when Rhizome already has a given manifest in the store. + * + * @author Andrew Bettison + */ +public class RhizomeDuplicateBundleException extends RhizomeException +{ + public RhizomeDuplicateBundleException(URL url) { + super("duplicate bundle", url); + } + +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeManifestAlreadyStoredException.java b/java/org/servalproject/servaldna/rhizome/RhizomeManifestAlreadyStoredException.java new file mode 100644 index 00000000..29a02f0f --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeManifestAlreadyStoredException.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.rhizome; + +import java.net.URL; + +/** + * Thrown when Rhizome already has a given manifest in the store. + * + * @author Andrew Bettison + */ +public class RhizomeManifestAlreadyStoredException extends RhizomeException +{ + public RhizomeManifestAlreadyStoredException(URL url) { + super("manifest already stored", url); + } + +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeManifestNotFoundException.java b/java/org/servalproject/servaldna/rhizome/RhizomeManifestNotFoundException.java new file mode 100644 index 00000000..18d85842 --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeManifestNotFoundException.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.rhizome; + +import java.net.URL; + +/** + * Thrown when Rhizome has no manifest with the given ID. + * + * @author Andrew Bettison + */ +public class RhizomeManifestNotFoundException extends RhizomeException +{ + public RhizomeManifestNotFoundException(URL url) { + super("manifest not found", url); + } + +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeOutdatedBundleException.java b/java/org/servalproject/servaldna/rhizome/RhizomeOutdatedBundleException.java new file mode 100644 index 00000000..9d0cdff5 --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeOutdatedBundleException.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.rhizome; + +import java.net.URL; + +/** + * Thrown when Rhizome has a newer manifest in the store, ie, same Bundle ID and higher version + * number. + * + * @author Andrew Bettison + */ +public class RhizomeOutdatedBundleException extends RhizomeException +{ + public RhizomeOutdatedBundleException(URL url) { + super("outdated bundle", url); + } + +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeStoreFullException.java b/java/org/servalproject/servaldna/rhizome/RhizomeStoreFullException.java new file mode 100644 index 00000000..ff0e6426 --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeStoreFullException.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.rhizome; + +import java.net.URL; + +/** + * Thrown when the Rhizome store is full. + * + * @author Andrew Bettison + */ +public class RhizomeStoreFullException extends RhizomeException +{ + public RhizomeStoreFullException(URL url) { + super("store is full", url); + } + +} diff --git a/java/org/servalproject/test/Rhizome.java b/java/org/servalproject/test/Rhizome.java index d66c62c4..9c9412cf 100644 --- a/java/org/servalproject/test/Rhizome.java +++ b/java/org/servalproject/test/Rhizome.java @@ -85,15 +85,19 @@ public class Rhizome { try { 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(); + if (bundle == null) + System.out.println("not found"); + else { + 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(); + } } catch (RhizomeException e) { System.out.println(e.toString()); @@ -104,23 +108,32 @@ public class Rhizome { static void rhizome_payload_raw(BundleId bid, String dstpath) throws ServalDInterfaceException, IOException, InterruptedException { ServalDClient client = new ServerControl().getRestfulClient(); - FileOutputStream out = new FileOutputStream(dstpath); + FileOutputStream out = null; try { RhizomePayloadRawBundle bundle = client.rhizomePayloadRaw(bid); - InputStream in = bundle.rawPayloadInputStream; - byte[] buf = new byte[4096]; - int n; - while ((n = in.read(buf)) > 0) - out.write(buf, 0, n); - in.close(); - out.close(); - out = null; - System.out.println( - "_insertTime=" + bundle.insertTime + "\n" + - "_author=" + bundle.author + "\n" + - "_secret=" + bundle.secret + "\n" + - manifestFields(bundle.manifest, "\n") + "\n" - ); + if (bundle == null) + System.out.println("not found"); + else { + InputStream in = bundle.rawPayloadInputStream; + if (in == null) + System.out.println("no payload"); + else { + out = new FileOutputStream(dstpath); + byte[] buf = new byte[4096]; + int n; + while ((n = in.read(buf)) > 0) + out.write(buf, 0, n); + in.close(); + out.close(); + out = null; + } + System.out.println( + "_insertTime=" + bundle.insertTime + "\n" + + "_author=" + bundle.author + "\n" + + "_secret=" + bundle.secret + "\n" + + manifestFields(bundle.manifest, "\n") + "\n" + ); + } } catch (RhizomeException e) { System.out.println(e.toString()); @@ -135,23 +148,32 @@ public class Rhizome { static void rhizome_payload_decrypted(BundleId bid, String dstpath) throws ServalDInterfaceException, IOException, InterruptedException { ServalDClient client = new ServerControl().getRestfulClient(); - FileOutputStream out = new FileOutputStream(dstpath); + FileOutputStream out = null; try { RhizomePayloadBundle bundle = client.rhizomePayload(bid); - InputStream in = bundle.payloadInputStream; - byte[] buf = new byte[4096]; - int n; - while ((n = in.read(buf)) > 0) - out.write(buf, 0, n); - in.close(); - out.close(); - out = null; - System.out.println( - "_insertTime=" + bundle.insertTime + "\n" + - "_author=" + bundle.author + "\n" + - "_secret=" + bundle.secret + "\n" + - manifestFields(bundle.manifest, "\n") + "\n" - ); + if (bundle == null) + System.out.println("not found"); + else { + InputStream in = bundle.payloadInputStream; + if (in == null) + System.out.println("no payload"); + else { + out = new FileOutputStream(dstpath); + byte[] buf = new byte[4096]; + int n; + while ((n = in.read(buf)) > 0) + out.write(buf, 0, n); + in.close(); + out.close(); + out = null; + } + System.out.println( + "_insertTime=" + bundle.insertTime + "\n" + + "_author=" + bundle.author + "\n" + + "_secret=" + bundle.secret + "\n" + + manifestFields(bundle.manifest, "\n") + "\n" + ); + } } catch (RhizomeException e) { System.out.println(e.toString()); diff --git a/rhizome_restful.c b/rhizome_restful.c index c05fb285..6b4b298e 100644 --- a/rhizome_restful.c +++ b/rhizome_restful.c @@ -708,7 +708,7 @@ static int restful_rhizome_bid_rhm(httpd_request *r, const char *remainder) if (*remainder) return 404; if (r->manifest == NULL) - return http_request_rhizome_response(r, 404, NULL, NULL); + return http_request_rhizome_response(r, 403, NULL, NULL); http_request_response_static(&r->http, 200, "rhizome-manifest/text", (const char *)r->manifest->manifestdata, r->manifest->manifest_all_bytes ); @@ -720,7 +720,7 @@ static int restful_rhizome_bid_raw_bin(httpd_request *r, const char *remainder) if (*remainder) return 404; if (r->manifest == NULL) - return http_request_rhizome_response(r, 404, NULL, NULL); + return http_request_rhizome_response(r, 403, NULL, NULL); if (r->manifest->filesize == 0) { http_request_response_static(&r->http, 200, CONTENT_TYPE_BLOB, "", 0); return 1; @@ -737,7 +737,7 @@ static int restful_rhizome_bid_decrypted_bin(httpd_request *r, const char *remai if (*remainder) return 404; if (r->manifest == NULL) - return http_request_rhizome_response(r, 404, NULL, NULL); + return http_request_rhizome_response(r, 403, NULL, NULL); if (r->manifest->filesize == 0) { // TODO use Content Type from manifest (once it is implemented) http_request_response_static(&r->http, 200, CONTENT_TYPE_BLOB, "", 0); diff --git a/tests/rhizomejava b/tests/rhizomejava index d1f698b8..64dc0800 100755 --- a/tests/rhizomejava +++ b/tests/rhizomejava @@ -31,6 +31,7 @@ setup() { executeOk_servald config \ set log.console.level debug \ set debug.httpd on + set_extra_config create_identities 4 start_servald_server } @@ -169,6 +170,17 @@ test_RhizomeManifest() { done } +doc_RhizomeManifestNonexist="Java API fetch non-existent Rhizome manifest" +setup_RhizomeManifestNonexist() { + setup +} +test_RhizomeManifestNonexist() { + executeJavaOk org.servalproject.test.Rhizome rhizome-manifest "$BID_NONEXISTENT" '' + tfw_cat --stdout --stderr + assertStdoutLineCount == 1 + assertStdoutGrep --ignore-case '^not found$' +} + doc_RhizomePayloadRaw="Java API fetch Rhizome raw payload" setup_RhizomePayloadRaw() { setup @@ -186,6 +198,33 @@ test_RhizomePayloadRaw() { done } +doc_RhizomePayloadRawNonexistManifest="Java API fetch Rhizome raw payload for non-existent manifest" +setup_RhizomePayloadRawNonexistManifest() { + setup +} +test_RhizomePayloadRawNonexistManifest() { + executeJavaOk org.servalproject.test.Rhizome rhizome-payload-raw "$BID_NONEXISTENT" '' + tfw_cat --stdout --stderr + assertStdoutLineCount == 1 + assertStdoutGrep --ignore-case '^not found$' +} + +doc_RhizomePayloadRawNonexistPayload="Java API fetch non-existent Rhizome raw payload" +setup_RhizomePayloadRawNonexistPayload() { + set_extra_config() { + executeOk_servald config set rhizome.max_blob_size 0 + } + setup + rhizome_add_bundles $SIDA1 0 0 + rhizome_delete_payload_blobs "${HASH[0]}" +} +test_RhizomePayloadRawNonexistPayload() { + executeJavaOk org.servalproject.test.Rhizome rhizome-payload-raw "${BID[0]}" raw.bin + tfw_cat --stdout --stderr + assertStdoutGrep --ignore-case '^no payload$' + assert_metadata 0 +} + doc_RhizomePayloadDecrypted="Java API fetch Rhizome decrypted payload" setup_RhizomePayloadDecrypted() { setup @@ -203,6 +242,22 @@ test_RhizomePayloadDecrypted() { done } +doc_RhizomePayloadDecryptedNonexistManifest="Java API fetch Rhizome decrypted payload for non-existent manifest" +setup_RhizomePayloadDecryptedNonexistManifest() { + set_extra_config() { + executeOk_servald config set rhizome.max_blob_size 0 + } + setup + rhizome_add_bundles $SIDA1 0 0 + rhizome_delete_payload_blobs "${HASH[0]}" +} +test_RhizomePayloadDecryptedNonexistManifest() { + executeJavaOk org.servalproject.test.Rhizome rhizome-payload-decrypted "${BID[0]}" '' + tfw_cat --stdout --stderr + assertStdoutGrep --ignore-case '^no payload$' + assert_metadata 0 +} + doc_RhizomePayloadDecryptedForeign="Java API cannot fetch foreign Rhizome decrypted payload" setup_RhizomePayloadDecryptedForeign() { setup diff --git a/tests/rhizomerestful b/tests/rhizomerestful index e4e3a7f2..acb93c12 100755 --- a/tests/rhizomerestful +++ b/tests/rhizomerestful @@ -115,7 +115,7 @@ teardown_AuthBasicWrong() { teardown } -doc_RhizomeList="HTTP RESTful list Rhizome bundles as JSON" +doc_RhizomeList="HTTP RESTful list 100 Rhizome bundles as JSON" setup_RhizomeList() { setup NBUNDLES=100 @@ -274,12 +274,12 @@ test_RhizomeManifestNonexist() { --basic --user harry:potter \ "http://$addr_localhost:$PORTA/restful/rhizome/$BID_NONEXISTENT.rhm" tfw_cat http.headers http.content - assertStdoutIs 404 + assertStdoutIs 403 assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Code: 0$CR\$" assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Message: .*bundle new to store.*$CR\$" assertGrep --matches=0 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Code:" assertGrep --matches=0 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Message:" - assertJq http.content 'contains({"http_status_code": 404})' + assertJq http.content 'contains({"http_status_code": 403})' assertJq http.content 'contains({"rhizome_bundle_status_code": 0})' assertJqGrep --ignore-case http.content '.rhizome_bundle_status_message' "bundle new to store" } @@ -322,12 +322,12 @@ test_RhizomePayloadRawNonexistManifest() { --basic --user harry:potter \ "http://$addr_localhost:$PORTA/restful/rhizome/$BID_NONEXISTENT/raw.bin" tfw_cat http.headers http.content - assertStdoutIs 404 + assertStdoutIs 403 assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Code: 0$CR\$" assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Message: .*bundle new to store.*$CR\$" assertGrep --matches=0 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Code:" assertGrep --matches=0 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Message:" - assertJq http.content 'contains({"http_status_code": 404})' + assertJq http.content 'contains({"http_status_code": 403})' assertJq http.content 'contains({"rhizome_bundle_status_code": 0})' assertJqGrep --ignore-case http.content '.rhizome_bundle_status_message' "bundle new to store" } @@ -425,12 +425,12 @@ test_RhizomePayloadDecryptedNonexistManifest() { --basic --user harry:potter \ "http://$addr_localhost:$PORTA/restful/rhizome/$BID_NONEXISTENT/decrypted.bin" tfw_cat http.headers http.content - assertStdoutIs 404 + assertStdoutIs 403 assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Code: 0$CR\$" assertGrep --matches=1 --ignore-case http.headers$n "^Serval-Rhizome-Result-Bundle-Status-Message: .*bundle new to store.*$CR\$" assertGrep --matches=0 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Code:" assertGrep --matches=0 --ignore-case http.headers$n "^Serval-Rhizome-Result-Payload-Status-Message:" - assertJq http.content 'contains({"http_status_code": 404})' + assertJq http.content 'contains({"http_status_code": 403})' assertJq http.content 'contains({"rhizome_bundle_status_code": 0})' assertJqGrep --ignore-case http.content '.rhizome_bundle_status_message' "bundle new to store" } From a81d05b4f6db84c732e7c2c59a7288e4d16fe29d Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Mon, 7 Jul 2014 17:54:52 +0930 Subject: [PATCH 15/21] Rhizome Java API: insert bundle --- .../servaldna/ServalDClient.java | 33 +++ .../rhizome/RhizomeBundleStatus.java | 4 +- .../servaldna/rhizome/RhizomeCommon.java | 220 ++++++++++++--- .../rhizome/RhizomeEncryptionException.java | 38 +++ .../servaldna/rhizome/RhizomeException.java | 5 + .../rhizome/RhizomeIncompleteManifest.java | 267 ++++++++++++++++++ .../rhizome/RhizomeInsertBundle.java | 42 +++ .../RhizomeInvalidManifestException.java | 10 +- .../servaldna/rhizome/RhizomeListBundle.java | 1 - .../servaldna/rhizome/RhizomeManifest.java | 161 +++-------- .../rhizome/RhizomeManifestBundle.java | 8 +- .../rhizome/RhizomePayloadBundle.java | 8 +- .../rhizome/RhizomePayloadRawBundle.java | 8 +- .../rhizome/RhizomeReadOnlyException.java | 37 +++ java/org/servalproject/test/Rhizome.java | 97 +++++-- rhizome_restful.c | 46 ++- testdefs_rhizome.sh | 10 +- tests/rhizomejava | 96 ++++++- 18 files changed, 878 insertions(+), 213 deletions(-) create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeEncryptionException.java create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeIncompleteManifest.java create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeInsertBundle.java create mode 100644 java/org/servalproject/servaldna/rhizome/RhizomeReadOnlyException.java diff --git a/java/org/servalproject/servaldna/ServalDClient.java b/java/org/servalproject/servaldna/ServalDClient.java index afec9e39..a5a89cb1 100644 --- a/java/org/servalproject/servaldna/ServalDClient.java +++ b/java/org/servalproject/servaldna/ServalDClient.java @@ -25,6 +25,7 @@ import org.servalproject.servaldna.meshms.MeshMSConversationList; import org.servalproject.servaldna.meshms.MeshMSException; import org.servalproject.servaldna.meshms.MeshMSMessageList; +import java.io.InputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URL; @@ -36,11 +37,19 @@ import org.servalproject.servaldna.BundleId; import org.servalproject.servaldna.ServalDCommand; import org.servalproject.servaldna.ServalDInterfaceException; import org.servalproject.servaldna.rhizome.RhizomeCommon; +import org.servalproject.servaldna.rhizome.RhizomeIncompleteManifest; import org.servalproject.servaldna.rhizome.RhizomeBundleList; import org.servalproject.servaldna.rhizome.RhizomeManifestBundle; import org.servalproject.servaldna.rhizome.RhizomePayloadRawBundle; import org.servalproject.servaldna.rhizome.RhizomePayloadBundle; +import org.servalproject.servaldna.rhizome.RhizomeInsertBundle; import org.servalproject.servaldna.rhizome.RhizomeException; +import org.servalproject.servaldna.rhizome.RhizomeInvalidManifestException; +import org.servalproject.servaldna.rhizome.RhizomeFakeManifestException; +import org.servalproject.servaldna.rhizome.RhizomeInconsistencyException; +import org.servalproject.servaldna.rhizome.RhizomeEncryptionException; +import org.servalproject.servaldna.rhizome.RhizomeReadOnlyException; +import org.servalproject.servaldna.rhizome.RhizomeDecryptionException; import org.servalproject.servaldna.meshms.MeshMSCommon; import org.servalproject.servaldna.meshms.MeshMSConversationList; import org.servalproject.servaldna.meshms.MeshMSMessageList; @@ -87,6 +96,30 @@ public class ServalDClient implements ServalDHttpConnectionFactory return RhizomeCommon.rhizomePayload(this, bid); } + public RhizomeInsertBundle rhizomeInsert(SubscriberId author, RhizomeIncompleteManifest manifest) + throws ServalDInterfaceException, + IOException, + RhizomeInvalidManifestException, + RhizomeFakeManifestException, + RhizomeInconsistencyException, + RhizomeReadOnlyException, + RhizomeEncryptionException + { + return RhizomeCommon.rhizomeInsert(this, author, manifest); + } + + public RhizomeInsertBundle rhizomeInsert(SubscriberId author, RhizomeIncompleteManifest manifest, InputStream payloadStream, String fileName) + throws ServalDInterfaceException, + IOException, + RhizomeInvalidManifestException, + RhizomeFakeManifestException, + RhizomeInconsistencyException, + RhizomeReadOnlyException, + RhizomeEncryptionException + { + return RhizomeCommon.rhizomeInsert(this, author, manifest, payloadStream, fileName); + } + public MeshMSConversationList meshmsListConversations(SubscriberId sid) throws ServalDInterfaceException, IOException, MeshMSException { MeshMSConversationList list = new MeshMSConversationList(this, sid); diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeBundleStatus.java b/java/org/servalproject/servaldna/rhizome/RhizomeBundleStatus.java index 92d817a4..16e5f60f 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeBundleStatus.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeBundleStatus.java @@ -33,7 +33,8 @@ public enum RhizomeBundleStatus { INVALID(4), // manifest is invalid FAKE(5), // manifest signature not valid INCONSISTENT(6), // manifest filesize/filehash does not match supplied payload - NO_ROOM(7) // doesn't fit; store may contain more important bundles + NO_ROOM(7), // doesn't fit; store may contain more important bundles + READONLY(8) // cannot modify manifest; secret unknown ; final public int code; @@ -55,6 +56,7 @@ public enum RhizomeBundleStatus { case 5: status = FAKE; break; case 6: status = INCONSISTENT; break; case 7: status = NO_ROOM; break; + case 8: status = READONLY; break; default: throw new InvalidException(code); } assert status.code == code; diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java index 30c81dc8..95f2ba5d 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java @@ -31,6 +31,7 @@ import java.io.IOException; import java.io.PrintStream; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; import java.net.URL; import java.net.HttpURLConnection; import org.servalproject.json.JSONTokeniser; @@ -57,6 +58,17 @@ public class RhizomeCommon String payload_status_message; } + private static void dumpStatus(Status status, PrintStream out) + { + out.println("input_stream=" + status.input_stream); + out.println("http_status_code=" + status.http_status_code); + out.println("http_status_message=" + status.http_status_message); + out.println("bundle_status_code=" + status.bundle_status_code); + out.println("bundle_status_message=" + status.bundle_status_message); + out.println("payload_status_code=" + status.payload_status_code); + out.println("payload_status_message=" + status.payload_status_message); + } + protected static Status receiveResponse(HttpURLConnection conn, int expected_response_code) throws IOException, ServalDInterfaceException { int[] expected_response_codes = { expected_response_code }; @@ -84,9 +96,9 @@ public class RhizomeCommon throw new ServalDInterfaceException("unexpected HTTP response code: " + conn.getResponseCode()); } - protected static void unexpectedResponse(Status status) throws ServalDInterfaceException + protected static ServalDInterfaceException unexpectedResponse(Status status) { - throw new ServalDInterfaceException( + return new ServalDInterfaceException( "unexpected Rhizome failure, \"" + status.http_status_message + "\"" + (status.bundle_status_code == null ? "" : ", " + status.bundle_status_code) + (status.bundle_status_message == null ? "" : " \"" + status.bundle_status_message + "\"") @@ -95,34 +107,6 @@ public class RhizomeCommon ); } -/* - protected static void throwRestfulResponseExceptions(Status status, URL url) throws RhizomeException, ServalDFailureException - { - if (status.bundle_status_code != null) { - switch (status.bundle_status_code) { - case ERROR: - throw new ServalDFailureException("received rhizome_bundle_status_code=ERROR(-1) from " + url); - case NEW: - throw new RhizomeManifestNotFoundException(url); - case SAME: - throw new RhizomeManifestAlreadyStoredException(url); - case DUPLICATE: - throw new RhizomeDuplicateBundleException(url); - case OLD: - throw new RhizomeOutdatedBundleException(url); - case NO_ROOM: - throw new RhizomeStoreFullException(url); - case INVALID: - throw new RhizomeInvalidManifestException(url); - case FAKE: - throw new RhizomeFakeManifestException(url); - case INCONSISTENT: - throw new RhizomeInconsistencyException(url); - } - } - } -*/ - protected static JSONTokeniser receiveRestfulResponse(HttpURLConnection conn, int expected_response_code) throws IOException, ServalDInterfaceException, RhizomeException { int[] expected_response_codes = { expected_response_code }; @@ -227,6 +211,7 @@ public class RhizomeCommon try { dumpHeaders(conn, System.err); decodeHeaderBundleStatus(status, conn); + dumpStatus(status, System.err); switch (status.bundle_status_code) { case NEW: return null; @@ -235,7 +220,7 @@ public class RhizomeCommon throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType()); RhizomeManifest manifest = RhizomeManifest.fromTextFormat(status.input_stream); BundleExtra extra = bundleExtraFromHeaders(conn); - return new RhizomeManifestBundle(manifest, extra.insertTime, extra.author, extra.secret); + return new RhizomeManifestBundle(manifest, extra.rowId, extra.insertTime, extra.author, extra.secret); case ERROR: throw new ServalDFailureException("received rhizome_bundle_status_code=ERROR(-1) from " + conn.getURL()); } @@ -247,8 +232,7 @@ public class RhizomeCommon if (status.input_stream != null) status.input_stream.close(); } - unexpectedResponse(status); - return null; + throw unexpectedResponse(status); } public static RhizomePayloadRawBundle rhizomePayloadRaw(ServalDHttpConnectionFactory connector, BundleId bid) @@ -260,6 +244,7 @@ public class RhizomeCommon try { dumpHeaders(conn, System.err); decodeHeaderBundleStatus(status, conn); + dumpStatus(status, System.err); switch (status.bundle_status_code) { case ERROR: throw new ServalDFailureException("received rhizome_bundle_status_code=ERROR(-1) from " + conn.getURL()); @@ -285,7 +270,7 @@ public class RhizomeCommon throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType()); RhizomeManifest manifest = manifestFromHeaders(conn); BundleExtra extra = bundleExtraFromHeaders(conn); - RhizomePayloadRawBundle ret = new RhizomePayloadRawBundle(manifest, status.input_stream, extra.insertTime, extra.author, extra.secret); + RhizomePayloadRawBundle ret = new RhizomePayloadRawBundle(manifest, status.input_stream, extra.rowId, extra.insertTime, extra.author, extra.secret); status.input_stream = null; // don't close when we return return ret; } @@ -296,8 +281,7 @@ public class RhizomeCommon if (status.input_stream != null) status.input_stream.close(); } - unexpectedResponse(status); - return null; + throw unexpectedResponse(status); } public static RhizomePayloadBundle rhizomePayload(ServalDHttpConnectionFactory connector, BundleId bid) @@ -309,6 +293,7 @@ public class RhizomeCommon try { dumpHeaders(conn, System.err); decodeHeaderBundleStatus(status, conn); + dumpStatus(status, System.err); switch (status.bundle_status_code) { case ERROR: throw new ServalDFailureException("received rhizome_bundle_status_code=ERROR(-1) from " + conn.getURL()); @@ -336,7 +321,7 @@ public class RhizomeCommon throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType()); RhizomeManifest manifest = manifestFromHeaders(conn); BundleExtra extra = bundleExtraFromHeaders(conn); - RhizomePayloadBundle ret = new RhizomePayloadBundle(manifest, status.input_stream, extra.insertTime, extra.author, extra.secret); + RhizomePayloadBundle ret = new RhizomePayloadBundle(manifest, status.input_stream, extra.rowId, extra.insertTime, extra.author, extra.secret); status.input_stream = null; // don't close when we return return ret; } @@ -347,8 +332,134 @@ public class RhizomeCommon if (status.input_stream != null) status.input_stream.close(); } - unexpectedResponse(status); - return null; + throw unexpectedResponse(status); + } + + public static RhizomeInsertBundle rhizomeInsert(ServalDHttpConnectionFactory connector, + SubscriberId author, + RhizomeIncompleteManifest manifest) + throws ServalDInterfaceException, + IOException, + RhizomeInvalidManifestException, + RhizomeFakeManifestException, + RhizomeInconsistencyException, + RhizomeReadOnlyException, + RhizomeEncryptionException + { + return rhizomeInsert(connector, author, manifest, null, null); + } + + public static RhizomeInsertBundle rhizomeInsert(ServalDHttpConnectionFactory connector, + SubscriberId author, + RhizomeIncompleteManifest manifest, + InputStream payloadStream, + String fileName) + throws ServalDInterfaceException, + IOException, + RhizomeInvalidManifestException, + RhizomeFakeManifestException, + RhizomeInconsistencyException, + RhizomeReadOnlyException, + RhizomeEncryptionException + { + HttpURLConnection conn = connector.newServalDHttpConnection("/restful/rhizome/insert"); + String boundary = Long.toHexString(System.currentTimeMillis()); + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); + conn.connect(); + OutputStream ost = conn.getOutputStream(); + PrintStream wr = new PrintStream(ost, false, "US-ASCII"); + if (author != null) { + wr.print("\r\n--" + boundary + "\r\n"); + wr.print("Content-Disposition: form-data; name=\"bundle-author\"\r\n"); + wr.print("\r\n"); + wr.print(author.toHex()); + } + wr.print("\r\n--" + boundary + "\r\n"); + wr.print("Content-Disposition: form-data; name=\"manifest\"\r\n"); + wr.print("Content-Type: rhizome-manifest/text\r\n"); + wr.print("\r\n"); + wr.flush(); + manifest.toTextFormat(ost); + if (payloadStream != null) { + wr.print("\r\n--" + boundary + "\r\n"); + wr.print("Content-Disposition: form-data; name=\"payload\""); + if (fileName != null) { + wr.print("; filename="); + wr.print(quoteString(fileName)); + } + wr.print("\r\n"); + wr.print("Content-Type: application/octet-stream\r\n"); + wr.print("\r\n"); + wr.flush(); + byte[] buffer = new byte[4096]; + int n; + while ((n = payloadStream.read(buffer)) > 0) + ost.write(buffer, 0, n); + } + wr.print("\r\n--" + boundary + "--\r\n"); + wr.close(); + int[] expected_response_codes = { HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_CREATED }; + Status status = RhizomeCommon.receiveResponse(conn, expected_response_codes); + try { + dumpHeaders(conn, System.err); + decodeHeaderPayloadStatus(status, conn); + switch (status.payload_status_code) { + case ERROR: + dumpStatus(status, System.err); + throw new ServalDFailureException("received rhizome_payload_status_code=ERROR(-1) from " + conn.getURL()); + case EMPTY: + case NEW: + case STORED: + decodeHeaderBundleStatus(status, conn); + dumpStatus(status, System.err); + switch (status.bundle_status_code) { + case ERROR: + throw new ServalDFailureException("received rhizome_bundle_status_code=ERROR(-1) from " + conn.getURL()); + case NEW: + case SAME: + case DUPLICATE: + case OLD: + case NO_ROOM: { + if (!conn.getContentType().equals("rhizome-manifest/text")) + throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType()); + RhizomeManifest returned_manifest = RhizomeManifest.fromTextFormat(status.input_stream); + BundleExtra extra = bundleExtraFromHeaders(conn); + return new RhizomeInsertBundle(status.bundle_status_code, returned_manifest, extra.rowId, extra.insertTime, extra.author, extra.secret); + } + case INVALID: + throw new RhizomeInvalidManifestException(conn.getURL()); + case FAKE: + throw new RhizomeFakeManifestException(conn.getURL()); + case INCONSISTENT: + throw new RhizomeInconsistencyException(conn.getURL()); + case READONLY: + throw new RhizomeReadOnlyException(conn.getURL()); + } + break; + case TOO_BIG: + case EVICTED: + dumpStatus(status, System.err); + return null; + case WRONG_SIZE: + case WRONG_HASH: + dumpStatus(status, System.err); + throw new RhizomeInconsistencyException(conn.getURL()); + case CRYPTO_FAIL: + dumpStatus(status, System.err); + throw new RhizomeEncryptionException(conn.getURL()); + } + } + catch (RhizomeManifestParseException e) { + throw new ServalDInterfaceException("malformed manifest from daemon", e); + } + finally { + if (status.input_stream != null) + status.input_stream.close(); + } + dumpStatus(status, System.err); + throw unexpectedResponse(status); } private static void dumpHeaders(HttpURLConnection conn, PrintStream out) @@ -376,7 +487,8 @@ public class RhizomeCommon } private static class BundleExtra { - public long insertTime; + public Long rowId; + public Long insertTime; public SubscriberId author; public BundleSecret secret; } @@ -384,15 +496,35 @@ public class RhizomeCommon private static BundleExtra bundleExtraFromHeaders(HttpURLConnection conn) throws ServalDInterfaceException { BundleExtra extra = new BundleExtra(); - extra.insertTime = headerUnsignedLong(conn, "Serval-Rhizome-Bundle-Inserttime"); - extra.author = header(conn, "Serval-Rhizome-Bundle-Author", SubscriberId.class); - extra.secret = header(conn, "Serval-Rhizome-Bundle-Secret", BundleSecret.class); + extra.rowId = headerUnsignedLongOrNull(conn, "Serval-Rhizome-Bundle-Rowid"); + extra.insertTime = headerUnsignedLongOrNull(conn, "Serval-Rhizome-Bundle-Inserttime"); + extra.author = headerOrNull(conn, "Serval-Rhizome-Bundle-Author", SubscriberId.class); + extra.secret = headerOrNull(conn, "Serval-Rhizome-Bundle-Secret", BundleSecret.class); return extra; } + private static String quoteString(String unquoted) + { + StringBuilder b = new StringBuilder(unquoted.length() + 2); + b.append('"'); + for (int i = 0; i < unquoted.length(); ++i) { + char c = unquoted.charAt(i); + if (c == '"' || c == '\\') + b.append('\\'); + b.append(c); + } + b.append('"'); + return b.toString(); + } + + private static String headerStringOrNull(HttpURLConnection conn, String header) throws ServalDInterfaceException + { + return conn.getHeaderField(header); + } + private static String headerString(HttpURLConnection conn, String header) throws ServalDInterfaceException { - String str = conn.getHeaderField(header); + String str = headerStringOrNull(conn, header); if (str == null) throw new ServalDInterfaceException("missing header field: " + header); return str; diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeEncryptionException.java b/java/org/servalproject/servaldna/rhizome/RhizomeEncryptionException.java new file mode 100644 index 00000000..d91b1ac6 --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeEncryptionException.java @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.rhizome; + +import java.net.URL; + +/** + * Thrown when a Rhizome API method is asked to encrypt a payload without possessing the necessary + * author or sender secret (not in keyring, or identity not unlocked) and without possessing the + * bundle secret. + * + * @author Andrew Bettison + */ +public class RhizomeEncryptionException extends RhizomeException +{ + public RhizomeEncryptionException(URL url) { + super("cannot encrypt payload", url); + } + +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeException.java b/java/org/servalproject/servaldna/rhizome/RhizomeException.java index 2b895791..8f5da3a6 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeException.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeException.java @@ -32,6 +32,11 @@ public abstract class RhizomeException extends Exception { public final URL url; + public RhizomeException(String message) { + super(message); + this.url = null; + } + public RhizomeException(String message, URL url) { super(message + "; " + url); this.url = url; diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeIncompleteManifest.java b/java/org/servalproject/servaldna/rhizome/RhizomeIncompleteManifest.java new file mode 100644 index 00000000..5dbc50a1 --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeIncompleteManifest.java @@ -0,0 +1,267 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.rhizome; + +import java.util.Map; +import java.util.HashMap; +import java.util.HashSet; +import java.io.UnsupportedEncodingException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import org.servalproject.servaldna.AbstractId; +import org.servalproject.servaldna.SubscriberId; +import org.servalproject.servaldna.BundleId; +import org.servalproject.servaldna.FileHash; +import org.servalproject.servaldna.BundleKey; + +public class RhizomeIncompleteManifest { + + public BundleId id; + public Long version; + public Long filesize; + public FileHash filehash; + public SubscriberId sender; + public SubscriberId recipient; + public BundleKey BK; + public Long tail; + public Integer crypt; + public Long date; + public String service; + public String name; + private HashMap extraFields; + + public RhizomeIncompleteManifest() + { + this.extraFields = new HashMap(); + } + + @SuppressWarnings("unchecked") + public RhizomeIncompleteManifest(RhizomeManifest m) + { + this.id = m.id; + this.version = m.version; + this.filesize = m.filesize; + this.filehash = m.filehash; + this.sender = m.sender; + this.recipient = m.recipient; + this.BK = m.BK; + this.crypt = m.crypt; + this.tail = m.tail; + this.date = m.date; + this.service = m.service; + this.name = m.name; + this.extraFields = (HashMap) m.extraFields.clone(); // unchecked cast + } + + /** Return the Rhizome manifest in its text format representation. + * + * @author Andrew Bettison + */ + public void toTextFormat(OutputStream os) throws IOException + { + OutputStreamWriter osw = new OutputStreamWriter(os, "US-ASCII"); + if (id != null) + osw.write("id=" + id.toHex() + "\n"); + if (version != null) + osw.write("version=" + version + "\n"); + if (filesize != null) + osw.write("filesize=" + filesize + "\n"); + if (filehash != null) + osw.write("filehash=" + filehash.toHex() + "\n"); + if (sender != null) + osw.write("sender=" + sender.toHex() + "\n"); + if (recipient != null) + osw.write("recipient=" + recipient.toHex() + "\n"); + if (BK != null) + osw.write("BK=" + BK.toHex() + "\n"); + if (crypt != null) + osw.write("crypt=" + crypt + "\n"); + if (tail != null) + osw.write("tail=" + tail + "\n"); + if (date != null) + osw.write("date=" + date + "\n"); + if (service != null) + osw.write("service=" + service + "\n"); + if (name != null) + osw.write("name=" + name + "\n"); + for (Map.Entry e: extraFields.entrySet()) + osw.write(e.getKey() + "=" + e.getValue() + "\n"); + osw.flush(); + } + + /** Construct a Rhizome manifest from its text format representation. + * + * @author Andrew Bettison + */ + public static RhizomeIncompleteManifest fromTextFormat(byte[] bytes) throws RhizomeManifestParseException + { + RhizomeIncompleteManifest m = new RhizomeIncompleteManifest(); + try { + m.parseTextFormat(new ByteArrayInputStream(bytes, 0, bytes.length)); + } + catch (IOException e) { + } + return m; + } + + /** Construct a Rhizome manifest from its text format representation. + * + * @author Andrew Bettison + */ + public static RhizomeIncompleteManifest fromTextFormat(byte[] bytes, int off, int len) throws RhizomeManifestParseException + { + RhizomeIncompleteManifest m = new RhizomeIncompleteManifest(); + try { + m.parseTextFormat(new ByteArrayInputStream(bytes, off, len)); + } + catch (IOException e) { + } + return m; + } + + /** Convenience method: construct a Rhizome manifest from all the bytes read from a given + * InputStream. + * + * @author Andrew Bettison + */ + static public RhizomeIncompleteManifest fromTextFormat(InputStream in) throws IOException, RhizomeManifestParseException + { + RhizomeIncompleteManifest m = new RhizomeIncompleteManifest(); + m.parseTextFormat(in); + return m; + } + + /** Fill in manifest fields from a text format representation. + * + * @author Andrew Bettison + */ + public void parseTextFormat(InputStream in) throws IOException, RhizomeManifestParseException + { + try { + InputStreamReader inr = new InputStreamReader(in, "US-ASCII"); + int pos = 0; + int lnum = 1; + int eq = -1; + StringBuilder line = new StringBuilder(); + while (true) { + int c = inr.read(); + if (c != -1 && c != '\n') { + if (eq == -1 && c == '=') + eq = line.length(); + line.append((char)c); + } + else if (line.length() == 0) + break; + else if (eq < 1) + throw new RhizomeManifestParseException("malformed (missing '=') at line " + lnum + ": " + line); + else { + String fieldName = line.substring(0, eq); + String fieldValue = line.substring(eq + 1); + if (!isFieldNameFirstChar(fieldName.charAt(0))) + throw new RhizomeManifestParseException("invalid field name at line " + lnum + ": " + line); + for (int i = 1; i < fieldName.length(); ++i) + if (!isFieldNameChar(fieldName.charAt(i))) + throw new RhizomeManifestParseException("invalid field name at line " + lnum + ": " + line); + try { + if (fieldName.equals("id")) + this.id = parseField(this.id, new BundleId(fieldValue)); + else if (fieldName.equals("version")) + this.version = parseField(this.version, parseUnsignedLong(fieldValue)); + else if (fieldName.equals("filesize")) + this.filesize = parseField(this.filesize, parseUnsignedLong(fieldValue)); + else if (fieldName.equals("filehash")) + this.filehash = parseField(this.filehash, new FileHash(fieldValue)); + else if (fieldName.equals("sender")) + this.sender = parseField(this.sender, new SubscriberId(fieldValue)); + else if (fieldName.equals("recipient")) + this.recipient = parseField(this.recipient, new SubscriberId(fieldValue)); + else if (fieldName.equals("BK")) + this.BK = parseField(this.BK, new BundleKey(fieldValue)); + else if (fieldName.equals("crypt")) + this.crypt = parseField(this.crypt, Integer.parseInt(fieldValue)); + else if (fieldName.equals("tail")) + this.tail = parseField(this.tail, parseUnsignedLong(fieldValue)); + else if (fieldName.equals("date")) + this.date = parseField(this.date, parseUnsignedLong(fieldValue)); + else if (fieldName.equals("service")) + this.service = parseField(this.service, fieldValue); + else if (fieldName.equals("name")) + this.name = parseField(this.name, fieldValue); + else if (this.extraFields.containsKey(fieldName)) + throw new RhizomeManifestParseException("duplicate field"); + else + this.extraFields.put(fieldName, fieldValue); + } + catch (RhizomeManifestParseException e) { + throw new RhizomeManifestParseException(e.getMessage() + " at line " + lnum + ": " + line); + } + catch (AbstractId.InvalidHexException e) { + throw new RhizomeManifestParseException("invalid value at line " + lnum + ": " + line, e); + } + catch (NumberFormatException e) { + throw new RhizomeManifestParseException("invalid value at line " + lnum + ": " + line, e); + } + line.setLength(0); + eq = -1; + ++lnum; + } + } + if (line.length() > 0) + throw new RhizomeManifestParseException("malformed (missing newline) at line " + lnum + ": " + line); + } + catch (UnsupportedEncodingException e) { + throw new RhizomeManifestParseException(e); + } + } + + static private T parseField(T currentValue, T newValue) throws RhizomeManifestParseException + { + if (currentValue == null) + return newValue; + if (!currentValue.equals(newValue)) + throw new RhizomeManifestParseException("duplicate field"); + return currentValue; + } + + private static boolean isFieldNameFirstChar(char c) + { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); + } + + private static boolean isFieldNameChar(char c) + { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'); + } + + private static Long parseUnsignedLong(String text) throws NumberFormatException + { + Long value = Long.valueOf(text); + if (value < 0) + throw new NumberFormatException("negative value not allowed"); + return value; + } + +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeInsertBundle.java b/java/org/servalproject/servaldna/rhizome/RhizomeInsertBundle.java new file mode 100644 index 00000000..c3b053ec --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeInsertBundle.java @@ -0,0 +1,42 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.rhizome; + +import org.servalproject.servaldna.SubscriberId; +import org.servalproject.servaldna.BundleSecret; +import org.servalproject.servaldna.ServalDInterfaceException; + +public class RhizomeInsertBundle extends RhizomeManifestBundle { + + public final RhizomeBundleStatus status; + + protected RhizomeInsertBundle(RhizomeBundleStatus status, + RhizomeManifest manifest, + Long rowId, + Long insertTime, + SubscriberId author, + BundleSecret secret) + { + super(manifest, rowId, insertTime, author, secret); + this.status = status; + } + +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeInvalidManifestException.java b/java/org/servalproject/servaldna/rhizome/RhizomeInvalidManifestException.java index f76bc01b..f1966218 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeInvalidManifestException.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeInvalidManifestException.java @@ -23,9 +23,9 @@ package org.servalproject.servaldna.rhizome; import java.net.URL; /** - * Thrown when a Rhizome API method is passed an invalid manifest. This is not an error within the - * Serval DNA interface, so it is not a subclass of ServalDInterfaceException. The programmer must - * explicitly deal with it instead of just absorbing it as an interface malfunction. + * Thrown when the Rhizome API rejects a caller-supplied manifest as invalid. This error does not + * originate from the Serval DNA interface, so it is not a subclass of ServalDInterfaceException. + * The programmer must deal with it, and not treat it as an interface malfunction. * * @author Andrew Bettison */ @@ -35,4 +35,8 @@ public class RhizomeInvalidManifestException extends RhizomeException super("invalid manifest", url); } + public RhizomeInvalidManifestException(RhizomeIncompleteManifest manifest) { + super("invalid manifest"); + } + } diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeListBundle.java b/java/org/servalproject/servaldna/rhizome/RhizomeListBundle.java index c5247fcd..22fec2e5 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeListBundle.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeListBundle.java @@ -39,7 +39,6 @@ public class RhizomeListBundle { long insertTime, SubscriberId author, int fromHere) - { this.manifest = manifest; this.rowNumber = rowNumber; diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeManifest.java b/java/org/servalproject/servaldna/rhizome/RhizomeManifest.java index c5fad0ac..08068f46 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeManifest.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeManifest.java @@ -26,8 +26,10 @@ import java.util.HashSet; import java.io.UnsupportedEncodingException; import java.io.IOException; import java.io.InputStream; -import java.io.ByteArrayOutputStream; +import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import org.servalproject.servaldna.AbstractId; import org.servalproject.servaldna.SubscriberId; import org.servalproject.servaldna.BundleId; @@ -56,7 +58,7 @@ public class RhizomeManifest { public final String service; public final String name; - private HashMap extraFields; + protected HashMap extraFields; private byte[] signatureBlock; private byte[] textFormat; @@ -96,59 +98,51 @@ public class RhizomeManifest { this.textFormat = null; } - /** Return the Rhizome manifest in its text format representation. + protected RhizomeManifest(RhizomeIncompleteManifest m) + { + this(m.id, m.version, m.filesize, m.filehash, m.sender, m.recipient, m.BK, m.crypt, m.tail, m.date, m.service, m.name); + } + + /** Return the Rhizome manifest in its text format representation, with the signature block at + * the end if present. * * @author Andrew Bettison */ public byte[] toTextFormat() throws RhizomeManifestSizeException { - if (textFormat == null) { + if (this.textFormat == null) { try { ByteArrayOutputStream os = new ByteArrayOutputStream(); - OutputStreamWriter osw = new OutputStreamWriter(os, "US-ASCII"); - osw.write("id=" + id.toHex() + "\n"); - osw.write("version=" + version + "\n"); - osw.write("filesize=" + filesize + "\n"); - if (filehash != null) - osw.write("filehash=" + filehash.toHex() + "\n"); - if (sender != null) - osw.write("sender=" + sender.toHex() + "\n"); - if (recipient != null) - osw.write("recipient=" + recipient.toHex() + "\n"); - if (BK != null) - osw.write("BK=" + BK.toHex() + "\n"); - if (crypt != null) - osw.write("crypt=" + crypt + "\n"); - if (tail != null) - osw.write("tail=" + tail + "\n"); - if (date != null) - osw.write("date=" + date + "\n"); - if (service != null) - osw.write("service=" + service + "\n"); - if (name != null) - osw.write("name=" + name + "\n"); - for (Map.Entry e: extraFields.entrySet()) - osw.write(e.getKey() + "=" + e.getValue() + "\n"); - osw.flush(); - if (signatureBlock != null) { - os.write(0); - os.write(signatureBlock); - } - osw.close(); + toTextFormat(os); + os.close(); if (os.size() > TEXT_FORMAT_MAX_SIZE) throw new RhizomeManifestSizeException("manifest text format overflow", os.size(), TEXT_FORMAT_MAX_SIZE); - textFormat = os.toByteArray(); + this.textFormat = os.toByteArray(); } catch (IOException e) { // should not happen with ByteArrayOutputStream return new byte[0]; } } - byte[] ret = new byte[textFormat.length]; - System.arraycopy(textFormat, 0, ret, 0, ret.length); + byte[] ret = new byte[this.textFormat.length]; + System.arraycopy(this.textFormat, 0, ret, 0, ret.length); return ret; } + /** Write the Rhizome manifest in its text format representation to the given output stream, + * with the signature block at the end if present. + * + * @author Andrew Bettison + */ + public void toTextFormat(OutputStream os) throws IOException + { + new RhizomeIncompleteManifest(this).toTextFormat(os); + if (this.signatureBlock != null) { + os.write(0); + os.write(this.signatureBlock); + } + } + /** Construct a Rhizome manifest from its text format representation. * * @author Andrew Bettison @@ -158,7 +152,8 @@ public class RhizomeManifest { return fromTextFormat(bytes, 0, bytes.length); } - /** Construct a Rhizome manifest from its text format representation. + /** Construct a complete Rhizome manifest from its text format representation, including a + * trailing signature block. * * @author Andrew Bettison */ @@ -175,95 +170,23 @@ public class RhizomeManifest { break; } } - String text; + RhizomeIncompleteManifest im = new RhizomeIncompleteManifest(); try { - text = new String(bytes, off, proplen, "US-ASCII"); + im.parseTextFormat(new ByteArrayInputStream(bytes, off, proplen)); } - catch (UnsupportedEncodingException e) { - throw new RhizomeManifestParseException(e); + catch (IOException e) { } - BundleId id = null; - Long version = null; - Long filesize = null; - FileHash filehash = null; - SubscriberId sender = null; - SubscriberId recipient = null; - BundleKey BK = null; - Integer crypt = null; - Long tail = null; - Long date = null; - String service = null; - String name = null; - HashMap extras = new HashMap(); - int pos = 0; - int lnum = 1; - while (pos < text.length()) { - int nl = text.indexOf('\n', pos); - if (nl == -1) - nl = text.length(); - int field = pos; - if (!isFieldNameFirstChar(text.charAt(field))) - throw new RhizomeManifestParseException("invalid field name at line " + lnum + ": " + text.substring(pos, nl - pos)); - ++field; - while (isFieldNameChar(text.charAt(field))) - ++field; - assert field < nl; - if (text.charAt(field) != '=') - throw new RhizomeManifestParseException("invalid field name at line " + lnum + ": " + text.substring(pos, nl - pos)); - String fieldName = text.substring(pos, field); - String fieldValue = text.substring(field + 1, nl); - HashSet fieldNames = new HashSet(50); - try { - if (fieldNames.contains(fieldName)) - throw new RhizomeManifestParseException("duplicate field at line " + lnum + ": " + text.substring(pos, nl - pos)); - fieldNames.add(fieldName); - if (fieldName.equals("id")) - id = new BundleId(fieldValue); - else if (fieldName.equals("version")) - version = parseUnsignedLong(fieldValue); - else if (fieldName.equals("filesize")) - filesize = parseUnsignedLong(fieldValue); - else if (fieldName.equals("filehash")) - filehash = new FileHash(fieldValue); - else if (fieldName.equals("sender")) - sender = new SubscriberId(fieldValue); - else if (fieldName.equals("recipient")) - recipient = new SubscriberId(fieldValue); - else if (fieldName.equals("BK")) - BK = new BundleKey(fieldValue); - else if (fieldName.equals("crypt")) - crypt = Integer.parseInt(fieldValue); - else if (fieldName.equals("tail")) - tail = parseUnsignedLong(fieldValue); - else if (fieldName.equals("date")) - date = parseUnsignedLong(fieldValue); - else if (fieldName.equals("service")) - service = fieldValue; - else if (fieldName.equals("name")) - name = fieldValue; - else - extras.put(fieldName, fieldValue); - } - catch (AbstractId.InvalidHexException e) { - throw new RhizomeManifestParseException("invalid value at line " + lnum + ": " + text.substring(pos, nl - pos), e); - } - catch (NumberFormatException e) { - throw new RhizomeManifestParseException("invalid value at line " + lnum + ": " + text.substring(pos, nl - pos), e); - } - pos = nl + 1; - } - if (id == null) + if (im.id == null) throw new RhizomeManifestParseException("missing 'id' field"); - if (version == null) + if (im.version == null) throw new RhizomeManifestParseException("missing 'version' field"); - if (filesize == null) + if (im.filesize == null) throw new RhizomeManifestParseException("missing 'filesize' field"); - if (filesize != 0 && filehash == null) + if (im.filesize != 0 && im.filehash == null) throw new RhizomeManifestParseException("missing 'filehash' field"); - else if (filesize == 0 && filehash != null) + else if (im.filesize == 0 && im.filehash != null) throw new RhizomeManifestParseException("spurious 'filehash' field"); - RhizomeManifest m = new RhizomeManifest(id, version, filesize, filehash, sender, recipient, BK, crypt, tail, date, service, name); - m.extraFields = extras; + RhizomeManifest m = new RhizomeManifest(im); m.signatureBlock = sigblock; m.textFormat = new byte[len]; System.arraycopy(bytes, off, m.textFormat, 0, m.textFormat.length); diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeManifestBundle.java b/java/org/servalproject/servaldna/rhizome/RhizomeManifestBundle.java index 452aa73d..0d10aeca 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeManifestBundle.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeManifestBundle.java @@ -26,18 +26,20 @@ import org.servalproject.servaldna.ServalDInterfaceException; public class RhizomeManifestBundle { - public final long insertTime; + public final Long insertTime; + public final Long rowId; public final SubscriberId author; public final BundleSecret secret; public final RhizomeManifest manifest; protected RhizomeManifestBundle(RhizomeManifest manifest, - long insertTime, + Long rowId, + Long insertTime, SubscriberId author, BundleSecret secret) - { this.manifest = manifest; + this.rowId = rowId; this.insertTime = insertTime; this.author = author; this.secret = secret; diff --git a/java/org/servalproject/servaldna/rhizome/RhizomePayloadBundle.java b/java/org/servalproject/servaldna/rhizome/RhizomePayloadBundle.java index 590eea0c..a65fd652 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomePayloadBundle.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomePayloadBundle.java @@ -29,19 +29,21 @@ public class RhizomePayloadBundle { public final RhizomeManifest manifest; public final InputStream payloadInputStream; - public final long insertTime; + public final Long rowId; + public final Long insertTime; public final SubscriberId author; public final BundleSecret secret; protected RhizomePayloadBundle(RhizomeManifest manifest, InputStream payloadInputStream, - long insertTime, + Long rowId, + Long insertTime, SubscriberId author, BundleSecret secret) - { this.payloadInputStream = payloadInputStream; this.manifest = manifest; + this.rowId = rowId; this.insertTime = insertTime; this.author = author; this.secret = secret; diff --git a/java/org/servalproject/servaldna/rhizome/RhizomePayloadRawBundle.java b/java/org/servalproject/servaldna/rhizome/RhizomePayloadRawBundle.java index ca00792d..64e081c3 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomePayloadRawBundle.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomePayloadRawBundle.java @@ -29,19 +29,21 @@ public class RhizomePayloadRawBundle { public final RhizomeManifest manifest; public final InputStream rawPayloadInputStream; - public final long insertTime; + public final Long rowId; + public final Long insertTime; public final SubscriberId author; public final BundleSecret secret; protected RhizomePayloadRawBundle(RhizomeManifest manifest, InputStream rawPayloadInputStream, - long insertTime, + Long rowId, + Long insertTime, SubscriberId author, BundleSecret secret) - { this.rawPayloadInputStream = rawPayloadInputStream; this.manifest = manifest; + this.rowId = rowId; this.insertTime = insertTime; this.author = author; this.secret = secret; diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeReadOnlyException.java b/java/org/servalproject/servaldna/rhizome/RhizomeReadOnlyException.java new file mode 100644 index 00000000..d910c77c --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeReadOnlyException.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.rhizome; + +import java.net.URL; + +/** + * Thrown when a Rhizome API method is passed a manifest which is inconsistent with a supplied + * payload. I.e., filesize or filehash does not match. + * + * @author Andrew Bettison + */ +public class RhizomeReadOnlyException extends RhizomeException +{ + public RhizomeReadOnlyException(URL url) { + super("bundle cannot be modified", url); + } + +} diff --git a/java/org/servalproject/test/Rhizome.java b/java/org/servalproject/test/Rhizome.java index 9c9412cf..0def1887 100644 --- a/java/org/servalproject/test/Rhizome.java +++ b/java/org/servalproject/test/Rhizome.java @@ -20,36 +20,42 @@ package org.servalproject.test; -import java.io.IOException; +import java.io.File; import java.io.InputStream; import java.io.OutputStream; +import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.IOException; import org.servalproject.servaldna.ServalDClient; import org.servalproject.servaldna.ServalDInterfaceException; import org.servalproject.servaldna.ServerControl; import org.servalproject.servaldna.BundleId; +import org.servalproject.servaldna.SubscriberId; import org.servalproject.servaldna.rhizome.RhizomeManifest; +import org.servalproject.servaldna.rhizome.RhizomeIncompleteManifest; import org.servalproject.servaldna.rhizome.RhizomeListBundle; import org.servalproject.servaldna.rhizome.RhizomeBundleList; import org.servalproject.servaldna.rhizome.RhizomeManifestBundle; import org.servalproject.servaldna.rhizome.RhizomePayloadRawBundle; import org.servalproject.servaldna.rhizome.RhizomePayloadBundle; +import org.servalproject.servaldna.rhizome.RhizomeInsertBundle; import org.servalproject.servaldna.rhizome.RhizomeException; +import org.servalproject.servaldna.rhizome.RhizomeManifestParseException; public class Rhizome { static String manifestFields(RhizomeManifest manifest, String sep) { - return "id=" + manifest.id + sep + - "version=" + manifest.version + sep + - "filesize=" + manifest.filesize + sep + - "filehash=" + manifest.filehash + sep + - "sender=" + manifest.sender + sep + - "recipient=" + manifest.recipient + sep + - "date=" + manifest.date + sep + - "service=" + manifest.service + sep + - "name=" + manifest.name + sep + - "BK=" + manifest.BK; + return "id=" + manifest.id + + sep + "version=" + manifest.version + + sep + "filesize=" + manifest.filesize + + (manifest.filesize != 0 ? sep + "filehash=" + manifest.filehash : "") + + (manifest.sender != null ? sep + "sender=" + manifest.sender : "") + + (manifest.recipient != null ? sep + "recipient=" + manifest.recipient : "") + + (manifest.date != null ? sep + "date=" + manifest.date : "") + + (manifest.service != null ? sep + "service=" + manifest.service : "") + + (manifest.BK != null ? sep + "BK=" + manifest.BK : "") + + (manifest.name != null ? sep + "name=" + manifest.name : ""); } static void rhizome_list() throws ServalDInterfaceException, IOException, InterruptedException @@ -61,8 +67,8 @@ public class Rhizome { RhizomeListBundle bundle; while ((bundle = list.nextBundle()) != null) { System.out.println( - "_rowId=" + bundle.rowId + - ", _token=" + bundle.token + + "_token=" + bundle.token + + ", _rowId=" + bundle.rowId + ", _insertTime=" + bundle.insertTime + ", _author=" + bundle.author + ", _fromHere=" + bundle.fromHere + @@ -89,9 +95,10 @@ public class Rhizome { System.out.println("not found"); else { System.out.println( - "_insertTime=" + bundle.insertTime + "\n" + - "_author=" + bundle.author + "\n" + - "_secret=" + bundle.secret + "\n" + + (bundle.rowId == null ? "" : "_rowId=" + bundle.rowId + "\n") + + (bundle.insertTime == null ? "" : "_insertTime=" + bundle.insertTime + "\n") + + (bundle.author == null ? "" : "_author=" + bundle.author + "\n") + + (bundle.secret == null ? "" : "_secret=" + bundle.secret + "\n") + manifestFields(bundle.manifest, "\n") + "\n" ); FileOutputStream out = new FileOutputStream(dstpath); @@ -128,9 +135,10 @@ public class Rhizome { out = null; } System.out.println( - "_insertTime=" + bundle.insertTime + "\n" + - "_author=" + bundle.author + "\n" + - "_secret=" + bundle.secret + "\n" + + (bundle.rowId == null ? "" : "_rowId=" + bundle.rowId + "\n") + + (bundle.insertTime == null ? "" : "_insertTime=" + bundle.insertTime + "\n") + + (bundle.author == null ? "" : "_author=" + bundle.author + "\n") + + (bundle.secret == null ? "" : "_secret=" + bundle.secret + "\n") + manifestFields(bundle.manifest, "\n") + "\n" ); } @@ -168,9 +176,10 @@ public class Rhizome { out = null; } System.out.println( - "_insertTime=" + bundle.insertTime + "\n" + - "_author=" + bundle.author + "\n" + - "_secret=" + bundle.secret + "\n" + + (bundle.rowId == null ? "" : "_rowId=" + bundle.rowId + "\n") + + (bundle.insertTime == null ? "" : "_insertTime=" + bundle.insertTime + "\n") + + (bundle.author == null ? "" : "_author=" + bundle.author + "\n") + + (bundle.secret == null ? "" : "_secret=" + bundle.secret + "\n") + manifestFields(bundle.manifest, "\n") + "\n" ); } @@ -185,6 +194,43 @@ public class Rhizome { System.exit(0); } + static void rhizome_insert(String author, String manifestpath, String payloadPath, String manifestoutpath, String payloadName) + throws ServalDInterfaceException, IOException, InterruptedException, SubscriberId.InvalidHexException + { + ServalDClient client = new ServerControl().getRestfulClient(); + try { + RhizomeIncompleteManifest manifest = RhizomeIncompleteManifest.fromTextFormat(new FileInputStream(manifestpath)); + RhizomeInsertBundle bundle; + SubscriberId authorSid = author == null || author.length() == 0 ? null : new SubscriberId(author); + if (payloadName == null || payloadName.length() == 0) + payloadName = new File(payloadPath).getName(); + if (payloadPath == null || payloadPath.length() == 0) + bundle = client.rhizomeInsert(authorSid, manifest); + else + bundle = client.rhizomeInsert(authorSid, manifest, new FileInputStream(payloadPath), payloadName); + System.out.println( + "_status=" + bundle.status + "\n" + + (bundle.rowId == null ? "" : "_rowId=" + bundle.rowId + "\n") + + (bundle.insertTime == null ? "" : "_insertTime=" + bundle.insertTime + "\n") + + (bundle.author == null ? "" : "_author=" + bundle.author + "\n") + + (bundle.secret == null ? "" : "_secret=" + bundle.secret + "\n") + + manifestFields(bundle.manifest, "\n") + "\n" + ); + if (manifestoutpath != null && manifestoutpath.length() != 0) { + FileOutputStream out = new FileOutputStream(manifestoutpath); + out.write(bundle.manifestText()); + out.close(); + } + } + catch (RhizomeManifestParseException e) { + System.out.println(e.toString()); + } + catch (RhizomeException e) { + System.out.println(e.toString()); + } + System.exit(0); + } + public static void main(String... args) { if (args.length < 1) @@ -199,6 +245,13 @@ public class Rhizome { rhizome_payload_raw(new BundleId(args[1]), args[2]); else if (methodName.equals("rhizome-payload-decrypted")) rhizome_payload_decrypted(new BundleId(args[1]), args[2]); + else if (methodName.equals("rhizome-insert")) + rhizome_insert( args[1], // author SID + args[2], // manifest path + args.length > 3 ? args[3] : null, // payload path + args.length > 4 ? args[4] : null, // manifest out path + args.length > 5 ? args[5] : null // payload name + ); } catch (Exception e) { e.printStackTrace(); System.exit(1); diff --git a/rhizome_restful.c b/rhizome_restful.c index 6b4b298e..67f7e123 100644 --- a/rhizome_restful.c +++ b/rhizome_restful.c @@ -376,16 +376,20 @@ static int insert_make_manifest(httpd_request *r) r->manifest->manifest_all_bytes = r->u.insert.manifest.length; int n = rhizome_manifest_parse(r->manifest); switch (n) { - case -1: - break; case 0: if (!r->manifest->malformed) return 0; // fall through case 1: + rhizome_manifest_free(r->manifest); + r->manifest = NULL; + r->bundle_status = RHIZOME_BUNDLE_STATUS_INVALID; return http_request_rhizome_response(r, 403, "Malformed manifest", NULL); default: WHYF("rhizome_manifest_parse() returned %d", n); + // fall through + case -1: + r->bundle_status = RHIZOME_BUNDLE_STATUS_ERROR; break; } } @@ -516,6 +520,8 @@ static int insert_mime_part_end(struct http_request *hr) } else if (r->u.insert.current_part == PART_MANIFEST) { r->u.insert.received_manifest = 1; + if (config.debug.rhizome) + DEBUGF("received %s = %s", PART_MANIFEST, alloca_toprint(-1, r->u.insert.manifest.buffer, r->u.insert.manifest.length)); int result = insert_make_manifest(r); if (result) return result; @@ -564,14 +570,14 @@ static int restful_rhizome_insert_end(struct http_request *hr) assert(r->manifest != NULL); assert(r->u.insert.write.file_length != RHIZOME_SIZE_UNSET); int status_valid = 0; + if (config.debug.rhizome) + DEBUGF("r->payload_status=%d", r->payload_status); switch (r->payload_status) { case RHIZOME_PAYLOAD_STATUS_NEW: - status_valid = 1; if (r->manifest->filesize == RHIZOME_SIZE_UNSET) rhizome_manifest_set_filesize(r->manifest, r->u.insert.write.file_length); // fall through case RHIZOME_PAYLOAD_STATUS_STORED: - status_valid = 1; // TODO: check that stored hash matches received payload's hash // fall through case RHIZOME_PAYLOAD_STATUS_EMPTY: @@ -579,18 +585,25 @@ static int restful_rhizome_insert_end(struct http_request *hr) assert(r->manifest->filesize != RHIZOME_SIZE_UNSET); if (r->u.insert.payload_size == r->manifest->filesize) break; + // fall through case RHIZOME_PAYLOAD_STATUS_WRONG_SIZE: r->payload_status = RHIZOME_PAYLOAD_STATUS_WRONG_SIZE; - status_valid = 1; + r->bundle_status = RHIZOME_BUNDLE_STATUS_INCONSISTENT; { strbuf msg = strbuf_alloca(200); strbuf_sprintf(msg, "Payload size (%"PRIu64") contradicts manifest (filesize=%"PRIu64")", r->u.insert.payload_size, r->manifest->filesize); return http_request_rhizome_response(r, 403, NULL, strbuf_str(msg)); } + case RHIZOME_PAYLOAD_STATUS_WRONG_HASH: + r->bundle_status = RHIZOME_BUNDLE_STATUS_INCONSISTENT; + return http_request_rhizome_response(r, 403, NULL, NULL); + case RHIZOME_PAYLOAD_STATUS_CRYPTO_FAIL: + r->bundle_status = RHIZOME_BUNDLE_STATUS_READONLY; + return http_request_rhizome_response(r, 403, "Missing bundle secret", NULL); case RHIZOME_PAYLOAD_STATUS_TOO_BIG: case RHIZOME_PAYLOAD_STATUS_EVICTED: - case RHIZOME_PAYLOAD_STATUS_WRONG_HASH: - case RHIZOME_PAYLOAD_STATUS_CRYPTO_FAIL: + r->bundle_status = RHIZOME_BUNDLE_STATUS_NO_ROOM; + // fall through case RHIZOME_PAYLOAD_STATUS_ERROR: return http_request_rhizome_response(r, 403, NULL, NULL); } @@ -605,17 +618,24 @@ static int restful_rhizome_insert_end(struct http_request *hr) else assert(cmp_rhizome_filehash_t(&r->u.insert.write.id, &r->manifest->filehash) == 0); } - if (!rhizome_manifest_validate(r->manifest) || r->manifest->malformed) { - http_request_simple_response(&r->http, 403, "Manifest is malformed"); - return 403; + const char *invalid_reason = rhizome_manifest_validate_reason(r->manifest); + if (invalid_reason) { + r->bundle_status = RHIZOME_BUNDLE_STATUS_INVALID; + return http_request_rhizome_response(r, 403, invalid_reason, NULL); + } + if (r->manifest->malformed) { + r->bundle_status = RHIZOME_BUNDLE_STATUS_INVALID; + return http_request_rhizome_response(r, 403, r->manifest->malformed, NULL); } if (!r->manifest->haveSecret) { - http_request_simple_response(&r->http, 403, "Missing bundle secret"); - return 403; + r->bundle_status = RHIZOME_BUNDLE_STATUS_READONLY; + return http_request_rhizome_response(r, 403, "Missing bundle secret", NULL); } rhizome_manifest *mout = NULL; r->bundle_status = rhizome_manifest_finalise(r->manifest, &mout, !r->u.insert.force_new); int result = 500; + if (config.debug.rhizome) + DEBUGF("r->bundle_status=%d", r->bundle_status); switch (r->bundle_status) { case RHIZOME_BUNDLE_STATUS_NEW: if (mout && mout != r->manifest) @@ -639,6 +659,8 @@ static int restful_rhizome_insert_end(struct http_request *hr) case RHIZOME_BUNDLE_STATUS_ERROR: if (mout && mout != r->manifest) rhizome_manifest_free(mout); + rhizome_manifest_free(r->manifest); + r->manifest = NULL; return http_request_rhizome_response(r, 0, NULL, NULL); } if (result == 500) diff --git a/testdefs_rhizome.sh b/testdefs_rhizome.sh index 443cc2fe..02e821f0 100644 --- a/testdefs_rhizome.sh +++ b/testdefs_rhizome.sh @@ -196,7 +196,7 @@ unpack_manifest_for_grep() { re_name=$(escape_grep_basic "${filename##*/}") if [ -e "$manifestname" ]; then re_filesize=$($SED -n -e '/^filesize=/s///p' "$manifestname") - if [ "$filesize" = 0 ]; then + if [ "$re_filesize" = 0 ]; then re_filehash= else re_filehash=$($SED -n -e '/^filehash=/s///p' "$manifestname") @@ -243,10 +243,18 @@ extract_stdout_version() { extract_stdout_keyvalue "$1" version "$rexp_version" } +extract_stdout_author_optional() { + extract_stdout_keyvalue_optional "$1" .author "$rexp_author" +} + extract_stdout_author() { extract_stdout_keyvalue "$1" .author "$rexp_author" } +extract_stdout_secret_optional() { + extract_stdout_keyvalue_optional "$1" .secret "$rexp_bundlesecret" +} + extract_stdout_secret() { extract_stdout_keyvalue "$1" .secret "$rexp_bundlesecret" } diff --git a/tests/rhizomejava b/tests/rhizomejava index 64dc0800..6f45f4ca 100755 --- a/tests/rhizomejava +++ b/tests/rhizomejava @@ -30,12 +30,18 @@ setup() { set_instance +A executeOk_servald config \ set log.console.level debug \ - set debug.httpd on + set debug.httpd on \ + set debug.rhizome on \ + set debug.rhizome_manifest on set_extra_config create_identities 4 start_servald_server } +set_extra_config() { + : +} + teardown() { stop_all_servald_servers kill_all_servald_processes @@ -165,6 +171,7 @@ test_RhizomeManifest() { executeJavaOk org.servalproject.test.Rhizome rhizome-manifest "${BID[$n]}" bundle$n.rhm tfw_cat --stdout --stderr assert_metadata $n + ls -l file$n.manifest bundle$n.rhm tfw_cat -v file$n.manifest -v bundle$n.rhm assert diff file$n.manifest bundle$n.rhm done @@ -275,4 +282,91 @@ test_RhizomePayloadDecryptedForeign() { assertStdoutGrep RhizomeDecryptionException } +doc_RhizomeInsert="Java API insert new Rhizome bundles" +setup_RhizomeInsert() { + setup + for n in 1 2 3 4; do + create_file file$n $((1000 + $n)) + create_file nfile$n $((1100 + $n)) + payload_filename[$n]= + eval author[$n]=\$SIDA$n + service[$n]=file + done + name[1]=elvis + echo "name=elvis" >manifest1 + name[2]=file2 + echo "crypt=1" >manifest2 + name[3]=fintlewoodlewix + payload_filename[3]=fintlewoodlewix + >manifest3 + name[4]= + author[4]= + service[4]=wah + echo -e "service=wah\ncrypt=0" >manifest4 +} +test_RhizomeInsert() { + for n in 1 2 3 4; do + executeJavaOk org.servalproject.test.Rhizome rhizome-insert "${author[$n]}" manifest$n file$n file$n.manifest "${payload_filename[$n]}" + tfw_cat --stdout --stderr -v file$n.manifest + assertStdoutGrep '^_status=NEW$' + replayStdout >stdout-insert + extract_manifest_id BID[$n] stdout-insert + extract_manifest SECRET[$n] stdout-insert _secret "$rexp_bundlesecret" + executeOk_servald rhizome extract bundle "${BID[$n]}" xfile$n.manifest xfile$n + tfw_cat --stdout -v xfile$n.manifest + extract_stdout_rowid ROWID[$n] + extract_stdout_inserttime INSERTTIME[$n] + assertGrep stdout-insert "^_rowId=${ROWID[$n]}\$" + assertGrep stdout-insert "^_insertTime=${INSERTTIME[$n]}\$" + if extract_stdout_author_optional AUTHOR[$n]; then + assertGrep stdout-insert "^_author=${AUTHOR[$n]}\$" + else + assertGrep --matches=0 stdout-insert "^_author=" + fi + assert diff xfile$n.manifest file$n.manifest + assert diff file$n xfile$n + unpack_manifest_for_grep xfile$n + assertGrep stdout-insert "^id=$re_manifestid\$" + assertGrep stdout-insert "^version=$re_version\$" + assertGrep stdout-insert "^filesize=$re_filesize\$" + if [ -n "$re_filehash" ]; then + assertGrep stdout-insert "^filehash=$re_filehash\$" + else + assertGrep --matches=0 stdout-insert "^filehash=" + fi + assertGrep stdout-insert "^date=$re_date\$" + assertGrep stdout-insert "^service=$re_service\$" + if [ -n "${name[$n]}" ]; then + assertGrep stdout-insert "^name=$re_name\$" + assert [ "$re_name" = "${name[$n]}" ] + fi + done + executeOk_servald rhizome list + assert_rhizome_list \ + --fromhere=1 \ + --author=${author[1]} file1 \ + --author=${author[2]} file2 \ + --author=${author[3]} file3 \ + --fromhere=0 \ + --author=${author[4]} file4 + for n in 1 2 3 4; do + $SED -e '/^version=/d;/^date=/d;/^filehash=/d;/^filesize=/d;/^[^a-zA-Z]/,$d' xfile$n.manifest >nmanifest$n + assertGrep nmanifest$n '^id=' + tfw_cat -v nmanifest$n + executeJavaOk org.servalproject.test.Rhizome rhizome-insert '' nmanifest$n nfile$n nfile$n.manifest "nfile$n" + tfw_cat --stdout --stderr -v nfile$n.manifest + if [ -n "${author[$n]}" ]; then + assertStdoutGrep '^_status=NEW$' + assertStdoutGrep "^id=${BID[$n]}\$" + assertStderrGrep --matches=1 "^bundle_status_code=NEW$CR\$" + assertStderrGrep --matches=1 --ignore-case "^bundle_status_message=.*bundle new to store.*$CR\$" + assertStderrGrep --matches=1 "^payload_status_code=NEW$CR\$" + assertStderrGrep --matches=1 --ignore-case "^payload_status_message=.*payload new to store.*$CR\$" + else + assertStdoutGrep RhizomeReadOnlyException + assertStderrGrep --ignore-case "missing bundle secret" + fi + done +} + runTests "$@" From 57cce64b6cdf988a3437ce2f98edc16492ae4e35 Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Thu, 10 Jul 2014 05:24:03 +0930 Subject: [PATCH 16/21] Rhizome Java API: narrower exceptions --- .../servaldna/ServalDClient.java | 9 ++-- .../servaldna/rhizome/RhizomeBundleList.java | 2 +- .../servaldna/rhizome/RhizomeCommon.java | 4 +- java/org/servalproject/test/Rhizome.java | 41 +++++++------------ 4 files changed, 22 insertions(+), 34 deletions(-) diff --git a/java/org/servalproject/servaldna/ServalDClient.java b/java/org/servalproject/servaldna/ServalDClient.java index a5a89cb1..8ee84d82 100644 --- a/java/org/servalproject/servaldna/ServalDClient.java +++ b/java/org/servalproject/servaldna/ServalDClient.java @@ -43,7 +43,6 @@ import org.servalproject.servaldna.rhizome.RhizomeManifestBundle; import org.servalproject.servaldna.rhizome.RhizomePayloadRawBundle; import org.servalproject.servaldna.rhizome.RhizomePayloadBundle; import org.servalproject.servaldna.rhizome.RhizomeInsertBundle; -import org.servalproject.servaldna.rhizome.RhizomeException; import org.servalproject.servaldna.rhizome.RhizomeInvalidManifestException; import org.servalproject.servaldna.rhizome.RhizomeFakeManifestException; import org.servalproject.servaldna.rhizome.RhizomeInconsistencyException; @@ -74,24 +73,24 @@ public class ServalDClient implements ServalDHttpConnectionFactory this.restfulPassword = restfulPassword; } - public RhizomeBundleList rhizomeListBundles() throws ServalDInterfaceException, IOException, RhizomeException + public RhizomeBundleList rhizomeListBundles() throws ServalDInterfaceException, IOException { RhizomeBundleList list = new RhizomeBundleList(this); list.connect(); return list; } - public RhizomeManifestBundle rhizomeManifest(BundleId bid) throws ServalDInterfaceException, IOException, RhizomeException + public RhizomeManifestBundle rhizomeManifest(BundleId bid) throws ServalDInterfaceException, IOException { return RhizomeCommon.rhizomeManifest(this, bid); } - public RhizomePayloadRawBundle rhizomePayloadRaw(BundleId bid) throws ServalDInterfaceException, IOException, RhizomeException + public RhizomePayloadRawBundle rhizomePayloadRaw(BundleId bid) throws ServalDInterfaceException, IOException { return RhizomeCommon.rhizomePayloadRaw(this, bid); } - public RhizomePayloadBundle rhizomePayload(BundleId bid) throws ServalDInterfaceException, IOException, RhizomeException + public RhizomePayloadBundle rhizomePayload(BundleId bid) throws ServalDInterfaceException, IOException, RhizomeDecryptionException { return RhizomeCommon.rhizomePayload(this, bid); } diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeBundleList.java b/java/org/servalproject/servaldna/rhizome/RhizomeBundleList.java index d1e1d6ff..47845bca 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeBundleList.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeBundleList.java @@ -68,7 +68,7 @@ public class RhizomeBundleList { return this.json != null; } - public void connect() throws IOException, ServalDInterfaceException, RhizomeException + public void connect() throws IOException, ServalDInterfaceException { try { rowCount = 0; diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java index 95f2ba5d..5f9892db 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java @@ -107,13 +107,13 @@ public class RhizomeCommon ); } - protected static JSONTokeniser receiveRestfulResponse(HttpURLConnection conn, int expected_response_code) throws IOException, ServalDInterfaceException, RhizomeException + protected static JSONTokeniser 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 JSONTokeniser receiveRestfulResponse(HttpURLConnection conn, int[] expected_response_codes) throws IOException, ServalDInterfaceException, RhizomeException + protected static JSONTokeniser receiveRestfulResponse(HttpURLConnection conn, int[] expected_response_codes) throws IOException, ServalDInterfaceException { Status status = receiveResponse(conn, expected_response_codes); if (!conn.getContentType().equals("application/json")) diff --git a/java/org/servalproject/test/Rhizome.java b/java/org/servalproject/test/Rhizome.java index 0def1887..b0255363 100644 --- a/java/org/servalproject/test/Rhizome.java +++ b/java/org/servalproject/test/Rhizome.java @@ -76,9 +76,6 @@ public class Rhizome { ); } } - catch (RhizomeException e) { - System.out.println(e.toString()); - } finally { if (list != null) list.close(); @@ -88,26 +85,21 @@ public class Rhizome { static void rhizome_manifest(BundleId bid, String dstpath) throws ServalDInterfaceException, IOException, InterruptedException { - try { - ServalDClient client = new ServerControl().getRestfulClient(); - RhizomeManifestBundle bundle = client.rhizomeManifest(bid); - if (bundle == null) - System.out.println("not found"); - else { - System.out.println( - (bundle.rowId == null ? "" : "_rowId=" + bundle.rowId + "\n") + - (bundle.insertTime == null ? "" : "_insertTime=" + bundle.insertTime + "\n") + - (bundle.author == null ? "" : "_author=" + bundle.author + "\n") + - (bundle.secret == null ? "" : "_secret=" + bundle.secret + "\n") + - manifestFields(bundle.manifest, "\n") + "\n" - ); - FileOutputStream out = new FileOutputStream(dstpath); - out.write(bundle.manifestText()); - out.close(); - } - } - catch (RhizomeException e) { - System.out.println(e.toString()); + ServalDClient client = new ServerControl().getRestfulClient(); + RhizomeManifestBundle bundle = client.rhizomeManifest(bid); + if (bundle == null) + System.out.println("not found"); + else { + System.out.println( + (bundle.rowId == null ? "" : "_rowId=" + bundle.rowId + "\n") + + (bundle.insertTime == null ? "" : "_insertTime=" + bundle.insertTime + "\n") + + (bundle.author == null ? "" : "_author=" + bundle.author + "\n") + + (bundle.secret == null ? "" : "_secret=" + bundle.secret + "\n") + + manifestFields(bundle.manifest, "\n") + "\n" + ); + FileOutputStream out = new FileOutputStream(dstpath); + out.write(bundle.manifestText()); + out.close(); } System.exit(0); } @@ -143,9 +135,6 @@ public class Rhizome { ); } } - catch (RhizomeException e) { - System.out.println(e.toString()); - } finally { if (out != null) out.close(); From 04b2a20e54fc8d55dc6187ad21cf3c533d385b3c Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Thu, 10 Jul 2014 21:12:40 +0930 Subject: [PATCH 17/21] Rhizome Java API: improved form-data headers Change manifest Content-Type from rhizome-manifest/text to rhizome/manifest; format="text+binarysig" Add "Content-Transfer-Encoding" form-part headers to Java API sent form parts, although not currently checked by Rhizome RESTful interface --- http_server.c | 20 +++++-- http_server.h | 1 + .../servaldna/rhizome/RhizomeCommon.java | 6 +- rhizome_restful.c | 6 +- strbuf_helpers.c | 4 ++ tests/rhizomerestful | 55 +++++++++++++------ 6 files changed, 68 insertions(+), 24 deletions(-) diff --git a/http_server.c b/http_server.c index 288dc716..1c90d768 100644 --- a/http_server.c +++ b/http_server.c @@ -637,6 +637,17 @@ static int _parse_content_type(struct http_request *r, struct mime_content_type continue; } r->cursor = start; + if (_skip_literal(r, "format=")) { + size_t n = _parse_token_or_quoted_string(r, ct->format, sizeof ct->format); + if (n == 0) + return 0; + if (n >= sizeof ct->format) { + WARNF("HTTP Content-Type format truncated: %s", alloca_str_toprint(ct->format)); + return 0; + } + continue; + } + r->cursor = start; struct substring param; if (_skip_token(r, ¶m) && _skip_literal(r, "=") && _parse_token_or_quoted_string(r, NULL, 0)) { if (r->debug_flag && *r->debug_flag) @@ -1337,11 +1348,10 @@ static int http_request_parse_body_form_data(struct http_request *r) char labelstr[labellen + 1]; strncpy(labelstr, label.start, labellen)[labellen] = '\0'; str_tolower_inplace(labelstr); - const char *value = r->cursor; if (strcmp(labelstr, "content-length") == 0) { if (r->part_header.content_length != CONTENT_LENGTH_UNKNOWN) { if (r->debug_flag && *r->debug_flag) - DEBUGF("Skipping duplicate HTTP multipart header Content-Length: %s", alloca_toprint(50, sol, r->end - sol)); + DEBUGF("Skipping duplicate HTTP multipart header %s", alloca_toprint(50, sol, r->end - sol)); return 400; } http_size_t length; @@ -1357,7 +1367,7 @@ static int http_request_parse_body_form_data(struct http_request *r) else if (strcmp(labelstr, "content-type") == 0) { if (r->part_header.content_type.type[0]) { if (r->debug_flag && *r->debug_flag) - DEBUGF("Skipping duplicate HTTP multipart header Content-Type: %s", alloca_toprint(50, sol, r->end - sol)); + DEBUGF("Skipping duplicate HTTP multipart header %s", alloca_toprint(50, sol, r->end - sol)); return 400; } if (_parse_content_type(r, &r->part_header.content_type) && _skip_optional_space(r) && _skip_crlf(r)) { @@ -1371,7 +1381,7 @@ static int http_request_parse_body_form_data(struct http_request *r) else if (strcmp(labelstr, "content-disposition") == 0) { if (r->part_header.content_disposition.type[0]) { if (r->debug_flag && *r->debug_flag) - DEBUGF("Skipping duplicate HTTP multipart header Content-Disposition: %s", alloca_toprint(50, sol, r->end - sol)); + DEBUGF("Skipping duplicate HTTP multipart header %s", alloca_toprint(50, sol, r->end - sol)); return 400; } if (_parse_content_disposition(r, &r->part_header.content_disposition) && _skip_optional_space(r) && _skip_crlf(r)) { @@ -1385,7 +1395,7 @@ static int http_request_parse_body_form_data(struct http_request *r) else if (_skip_to_crlf(r)) { _commit(r); if (r->debug_flag && *r->debug_flag) - DEBUGF("Skip HTTP multipart header: %s: %s", alloca_str_toprint(labelstr), alloca_toprint(-1, value, value - r->cursor)); + DEBUGF("Skip HTTP multipart header: %s", alloca_toprint(50, sol, r->parsed - sol)); return 0; } } diff --git a/http_server.h b/http_server.h index 7de5bb9c..b57aff1b 100644 --- a/http_server.h +++ b/http_server.h @@ -68,6 +68,7 @@ struct mime_content_type { char subtype[64]; char multipart_boundary[71]; char charset[31]; + char format[31]; }; struct http_client_authorization { diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java index 5f9892db..7106fe53 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java @@ -373,12 +373,15 @@ public class RhizomeCommon if (author != null) { wr.print("\r\n--" + boundary + "\r\n"); wr.print("Content-Disposition: form-data; name=\"bundle-author\"\r\n"); + wr.print("Content-Type: serval-mesh/sid\r\n"); + wr.print("Content-Transfer-Encoding: hex\r\n"); wr.print("\r\n"); wr.print(author.toHex()); } wr.print("\r\n--" + boundary + "\r\n"); wr.print("Content-Disposition: form-data; name=\"manifest\"\r\n"); - wr.print("Content-Type: rhizome-manifest/text\r\n"); + wr.print("Content-Type: rhizome/manifest; format=\"text+binarysig\"\r\n"); + wr.print("Content-Transfer-Encoding: binary\r\n"); wr.print("\r\n"); wr.flush(); manifest.toTextFormat(ost); @@ -391,6 +394,7 @@ public class RhizomeCommon } wr.print("\r\n"); wr.print("Content-Type: application/octet-stream\r\n"); + wr.print("Content-Transfer-Encoding: binary\r\n"); wr.print("\r\n"); wr.flush(); byte[] buffer = new byte[4096]; diff --git a/rhizome_restful.c b/rhizome_restful.c index 67f7e123..8da5929e 100644 --- a/rhizome_restful.c +++ b/rhizome_restful.c @@ -418,10 +418,12 @@ static int insert_mime_part_header(struct http_request *hr, const struct mime_pa if (r->u.insert.received_manifest) return http_response_form_part(r, "Duplicate", PART_MANIFEST, NULL, 0); form_buf_malloc_init(&r->u.insert.manifest, MAX_MANIFEST_BYTES); - if ( strcmp(h->content_type.type, "rhizome-manifest") != 0 - || strcmp(h->content_type.subtype, "text") != 0 + if ( strcmp(h->content_type.type, "rhizome") != 0 + || strcmp(h->content_type.subtype, "manifest") != 0 ) return http_response_form_part(r, "Unsupported Content-Type in", PART_MANIFEST, NULL, 0); + if (strcmp(h->content_type.format, "text+binarysig") != 0) + return http_response_form_part(r, "Unsupported rhizome/manifest format in", PART_MANIFEST, NULL, 0); r->u.insert.current_part = PART_MANIFEST; } else if (strcmp(h->content_disposition.name, PART_PAYLOAD) == 0) { diff --git a/strbuf_helpers.c b/strbuf_helpers.c index 05c271e8..140957de 100644 --- a/strbuf_helpers.c +++ b/strbuf_helpers.c @@ -813,6 +813,10 @@ strbuf strbuf_append_mime_content_type(strbuf sb, const struct mime_content_type strbuf_puts(sb, "; boundary="); strbuf_append_quoted_string(sb, ct->multipart_boundary); } + if (ct->format) { + strbuf_puts(sb, "; format="); + strbuf_append_quoted_string(sb, ct->format); + } return sb; } diff --git a/tests/rhizomerestful b/tests/rhizomerestful index acb93c12..8fe06aa2 100755 --- a/tests/rhizomerestful +++ b/tests/rhizomerestful @@ -515,7 +515,7 @@ test_RhizomeInsert() { --dump-header http.header$n \ --basic --user harry:potter \ "${authorargs[@]}" \ - --form "manifest=@manifest$n;type=rhizome-manifest/text" \ + --form "manifest=@manifest$n;type=rhizome/manifest;format=\"text+binarysig\"" \ --form "payload=@file$n${payload_filename[$n]:+;filename=\"${payload_filename[$n]}\"}" \ "http://$addr_localhost:$PORTA/restful/rhizome/insert" tfw_cat http.header$n file$n.manifest @@ -579,7 +579,7 @@ test_RhizomeInsert() { --output nfile$n.manifest \ --dump-header http.headers$n \ --basic --user harry:potter \ - --form "manifest=@nmanifest$n;type=rhizome-manifest/text" \ + --form "manifest=@nmanifest$n;type=rhizome/manifest;format=\"text+binarysig\"" \ --form "payload=@nfile$n;filename=\"nfile$n\"" \ "http://$addr_localhost:$PORTA/restful/rhizome/insert" tfw_cat http.headers$n nfile$n.manifest @@ -616,7 +616,7 @@ test_RhizomeInsertAnon() { --dump-header http.header \ --basic --user harry:potter \ --form "bundle-secret=$SECRET" \ - --form "manifest=@file2.manifest;type=rhizome-manifest/text" \ + --form "manifest=@file2.manifest;type=rhizome/manifest;format=\"text+binarysig\"" \ --form "payload=@file2" \ "http://$addr_localhost:$PORTA/restful/rhizome/insert" tfw_cat http.header ifile2.manifest @@ -642,7 +642,7 @@ test_RhizomeInsertEmpty() { --output empty.manifest \ --dump-header http.header \ --basic --user harry:potter \ - --form "manifest=;type=rhizome-manifest/text" \ + --form "manifest=;type=rhizome/manifest;format=\"text+binarysig\"" \ --form "payload=@empty;filename=\"lucky\"" \ "http://$addr_localhost:$PORTA/restful/rhizome/insert" tfw_cat http.header empty.manifest @@ -671,7 +671,7 @@ test_RhizomeInsertLarge() { --output file1.manifest \ --dump-header http.header \ --basic --user harry:potter \ - --form "manifest=;type=rhizome-manifest/text" \ + --form "manifest=;type=rhizome/manifest;format=\"text+binarysig\"" \ --form "payload=@file1" \ "http://$addr_localhost:$PORTA/restful/rhizome/insert" tfw_cat http.header -v file1.manifest @@ -722,7 +722,7 @@ test_RhizomeInsertIncorrectManifestType() { --output http.body \ --dump-header http.header \ --basic --user harry:potter \ - --form "manifest=;type=rhizome-manifest/something" \ + --form "manifest=;type=rhizome-manifest/text" \ --form "payload=@file1" \ "http://$addr_localhost:$PORTA/restful/rhizome/insert" tfw_cat http.header http.body @@ -734,6 +734,29 @@ test_RhizomeInsertIncorrectManifestType() { assert_rhizome_list } +doc_RhizomeInsertIncorrectManifestFormat="HTTP RESTful insert Rhizome bundle, incorrect 'manifest' content format" +setup_RhizomeInsertIncorrectManifestFormat() { + setup + echo 'File one' >file1 +} +test_RhizomeInsertIncorrectManifestFormat() { + execute curl \ + --silent --show-error --write-out '%{http_code}' \ + --output http.body \ + --dump-header http.header \ + --basic --user harry:potter \ + --form "manifest=;type=rhizome/manifest;format=\"text\"" \ + --form "payload=@file1" \ + "http://$addr_localhost:$PORTA/restful/rhizome/insert" + tfw_cat http.header http.body + assertExitStatus == 0 + assertStdoutIs 403 + assertJq http.body 'contains({"http_status_code": 403})' + assertJqGrep --ignore-case http.body '.http_status_message' 'unsupported.*format.*manifest.*form.*part' + executeOk_servald rhizome list + assert_rhizome_list +} + doc_RhizomeInsertDuplicateManifest="HTTP RESTful insert Rhizome bundle, duplicate 'manifest' form part" setup_RhizomeInsertDuplicateManifest() { setup @@ -747,8 +770,8 @@ test_RhizomeInsertDuplicateManifest() { --output http.body \ --dump-header http.header \ --basic --user harry:potter \ - --form "manifest=@file1.manifest;type=rhizome-manifest/text" \ - --form "manifest=@file2.manifest;type=rhizome-manifest/text" \ + --form "manifest=@file1.manifest;type=rhizome/manifest;format=\"text+binarysig\"" \ + --form "manifest=@file2.manifest;type=rhizome/manifest;format=\"text+binarysig\"" \ --form "payload=@file1" \ "http://$addr_localhost:$PORTA/restful/rhizome/insert" tfw_cat http.header http.body @@ -772,7 +795,7 @@ test_RhizomeInsertJournal() { --output http.body \ --dump-header http.header \ --basic --user harry:potter \ - --form "manifest=@file1.manifest;type=rhizome-manifest/text" \ + --form "manifest=@file1.manifest;type=rhizome/manifest;format=\"text+binarysig\"" \ --form "payload=@file1" \ "http://$addr_localhost:$PORTA/restful/rhizome/insert" tfw_cat http.header http.body @@ -796,7 +819,7 @@ test_RhizomeInsertMissingPayload() { --output http.body \ --dump-header http.header \ --basic --user harry:potter \ - --form "manifest=@file1.manifest;type=rhizome-manifest/text" \ + --form "manifest=@file1.manifest;type=rhizome/manifest;format=\"text+binarysig\"" \ "http://$addr_localhost:$PORTA/restful/rhizome/insert" tfw_cat http.header http.body assertExitStatus == 0 @@ -820,7 +843,7 @@ test_RhizomeInsertDuplicatePayload() { --output http.body \ --dump-header http.header \ --basic --user harry:potter \ - --form "manifest=@file1.manifest;type=rhizome-manifest/text" \ + --form "manifest=@file1.manifest;type=rhizome/manifest;format=\"text+binarysig\"" \ --form "payload=@file1" \ --form "payload=@file2" \ "http://$addr_localhost:$PORTA/restful/rhizome/insert" @@ -846,7 +869,7 @@ test_RhizomeInsertPartOrder() { --dump-header http.header \ --basic --user harry:potter \ --form "payload=@file1" \ - --form "manifest=@file1.manifest;type=rhizome-manifest/text" \ + --form "manifest=@file1.manifest;type=rhizome/manifest;format=\"text+binarysig\"" \ "http://$addr_localhost:$PORTA/restful/rhizome/insert" tfw_cat http.header http.body assertExitStatus == 0 @@ -869,7 +892,7 @@ test_RhizomeInsertPartUnsupported() { --output http.body \ --dump-header http.header \ --basic --user harry:potter \ - --form "manifest=@file1.manifest;type=rhizome-manifest/text" \ + --form "manifest=@file1.manifest;type=rhizome/manifest;format=\"text+binarysig\"" \ --form "payload=@file1" \ --form "happyhappy=joyjoy" \ "http://$addr_localhost:$PORTA/restful/rhizome/insert" @@ -897,7 +920,7 @@ test_RhizomeInsertIncorrectFilesize() { --output http.body \ --dump-header http.header \ --basic --user harry:potter \ - --form "manifest=@file1.manifest;type=rhizome-manifest/text" \ + --form "manifest=@file1.manifest;type=rhizome/manifest;format=\"text+binarysig\"" \ --form "payload=@file1" \ "http://$addr_localhost:$PORTA/restful/rhizome/insert" tfw_cat http.header http.body @@ -911,7 +934,7 @@ test_RhizomeInsertIncorrectFilesize() { --output http.body \ --dump-header http.header \ --basic --user harry:potter \ - --form "manifest=@file2.manifest;type=rhizome-manifest/text" \ + --form "manifest=@file2.manifest;type=rhizome/manifest;format=\"text+binarysig\"" \ --form "payload=@file2" \ "http://$addr_localhost:$PORTA/restful/rhizome/insert" tfw_cat http.header http.body @@ -936,7 +959,7 @@ test_RhizomeInsertIncorrectFilehash() { --output http.body \ --dump-header http.header \ --basic --user harry:potter \ - --form "manifest=@file1.manifest;type=rhizome-manifest/text" \ + --form "manifest=@file1.manifest;type=rhizome/manifest;format=\"text+binarysig\"" \ --form "payload=@file1" \ "http://$addr_localhost:$PORTA/restful/rhizome/insert" tfw_cat http.header http.body From a87133d4d361edd602617092f54040e21f5335a3 Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Thu, 10 Jul 2014 21:19:15 +0930 Subject: [PATCH 18/21] Rhizome Java API: insert anonymous bundle --- .../servaldna/ServalDClient.java | 9 +++--- .../servaldna/rhizome/RhizomeCommon.java | 14 ++++++++-- java/org/servalproject/test/Rhizome.java | 25 +++++++++++++---- tests/rhizomejava | 28 +++++++++++++++++-- 4 files changed, 62 insertions(+), 14 deletions(-) diff --git a/java/org/servalproject/servaldna/ServalDClient.java b/java/org/servalproject/servaldna/ServalDClient.java index 8ee84d82..dd7a895c 100644 --- a/java/org/servalproject/servaldna/ServalDClient.java +++ b/java/org/servalproject/servaldna/ServalDClient.java @@ -34,6 +34,7 @@ import java.net.HttpURLConnection; import org.servalproject.codec.Base64; import org.servalproject.servaldna.SubscriberId; import org.servalproject.servaldna.BundleId; +import org.servalproject.servaldna.BundleSecret; import org.servalproject.servaldna.ServalDCommand; import org.servalproject.servaldna.ServalDInterfaceException; import org.servalproject.servaldna.rhizome.RhizomeCommon; @@ -95,7 +96,7 @@ public class ServalDClient implements ServalDHttpConnectionFactory return RhizomeCommon.rhizomePayload(this, bid); } - public RhizomeInsertBundle rhizomeInsert(SubscriberId author, RhizomeIncompleteManifest manifest) + public RhizomeInsertBundle rhizomeInsert(SubscriberId author, RhizomeIncompleteManifest manifest, BundleSecret secret) throws ServalDInterfaceException, IOException, RhizomeInvalidManifestException, @@ -104,10 +105,10 @@ public class ServalDClient implements ServalDHttpConnectionFactory RhizomeReadOnlyException, RhizomeEncryptionException { - return RhizomeCommon.rhizomeInsert(this, author, manifest); + return RhizomeCommon.rhizomeInsert(this, author, manifest, secret); } - public RhizomeInsertBundle rhizomeInsert(SubscriberId author, RhizomeIncompleteManifest manifest, InputStream payloadStream, String fileName) + public RhizomeInsertBundle rhizomeInsert(SubscriberId author, RhizomeIncompleteManifest manifest, BundleSecret secret, InputStream payloadStream, String fileName) throws ServalDInterfaceException, IOException, RhizomeInvalidManifestException, @@ -116,7 +117,7 @@ public class ServalDClient implements ServalDHttpConnectionFactory RhizomeReadOnlyException, RhizomeEncryptionException { - return RhizomeCommon.rhizomeInsert(this, author, manifest, payloadStream, fileName); + return RhizomeCommon.rhizomeInsert(this, author, manifest, secret, payloadStream, fileName); } public MeshMSConversationList meshmsListConversations(SubscriberId sid) throws ServalDInterfaceException, IOException, MeshMSException diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java index 7106fe53..2341feca 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java @@ -337,7 +337,8 @@ public class RhizomeCommon public static RhizomeInsertBundle rhizomeInsert(ServalDHttpConnectionFactory connector, SubscriberId author, - RhizomeIncompleteManifest manifest) + RhizomeIncompleteManifest manifest, + BundleSecret secret) throws ServalDInterfaceException, IOException, RhizomeInvalidManifestException, @@ -346,12 +347,13 @@ public class RhizomeCommon RhizomeReadOnlyException, RhizomeEncryptionException { - return rhizomeInsert(connector, author, manifest, null, null); + return rhizomeInsert(connector, author, manifest, secret, null, null); } public static RhizomeInsertBundle rhizomeInsert(ServalDHttpConnectionFactory connector, SubscriberId author, RhizomeIncompleteManifest manifest, + BundleSecret secret, InputStream payloadStream, String fileName) throws ServalDInterfaceException, @@ -378,6 +380,14 @@ public class RhizomeCommon wr.print("\r\n"); wr.print(author.toHex()); } + if (secret != null) { + wr.print("\r\n--" + boundary + "\r\n"); + wr.print("Content-Disposition: form-data; name=\"bundle-secret\"\r\n"); + wr.print("Content-Type: rhizome/bundle-secret\r\n"); + wr.print("Content-Transfer-Encoding: hex\r\n"); + wr.print("\r\n"); + wr.print(secret.toHex()); + } wr.print("\r\n--" + boundary + "\r\n"); wr.print("Content-Disposition: form-data; name=\"manifest\"\r\n"); wr.print("Content-Type: rhizome/manifest; format=\"text+binarysig\"\r\n"); diff --git a/java/org/servalproject/test/Rhizome.java b/java/org/servalproject/test/Rhizome.java index b0255363..dc5603ca 100644 --- a/java/org/servalproject/test/Rhizome.java +++ b/java/org/servalproject/test/Rhizome.java @@ -30,6 +30,7 @@ import org.servalproject.servaldna.ServalDClient; import org.servalproject.servaldna.ServalDInterfaceException; import org.servalproject.servaldna.ServerControl; import org.servalproject.servaldna.BundleId; +import org.servalproject.servaldna.BundleSecret; import org.servalproject.servaldna.SubscriberId; import org.servalproject.servaldna.rhizome.RhizomeManifest; import org.servalproject.servaldna.rhizome.RhizomeIncompleteManifest; @@ -183,20 +184,31 @@ public class Rhizome { System.exit(0); } - static void rhizome_insert(String author, String manifestpath, String payloadPath, String manifestoutpath, String payloadName) - throws ServalDInterfaceException, IOException, InterruptedException, SubscriberId.InvalidHexException + static void rhizome_insert( String author, + String manifestPath, + String payloadPath, + String manifestoutpath, + String payloadName, + String secretHex) + throws ServalDInterfaceException, + IOException, + InterruptedException, + SubscriberId.InvalidHexException { ServalDClient client = new ServerControl().getRestfulClient(); try { - RhizomeIncompleteManifest manifest = RhizomeIncompleteManifest.fromTextFormat(new FileInputStream(manifestpath)); + RhizomeIncompleteManifest manifest = new RhizomeIncompleteManifest(); + if (manifestPath != null && manifestPath.length() != 0) + manifest.parseTextFormat(new FileInputStream(manifestPath)); RhizomeInsertBundle bundle; SubscriberId authorSid = author == null || author.length() == 0 ? null : new SubscriberId(author); + BundleSecret secret = secretHex == null || secretHex.length() == 0 ? null : new BundleSecret(secretHex); if (payloadName == null || payloadName.length() == 0) payloadName = new File(payloadPath).getName(); if (payloadPath == null || payloadPath.length() == 0) - bundle = client.rhizomeInsert(authorSid, manifest); + bundle = client.rhizomeInsert(authorSid, manifest, secret); else - bundle = client.rhizomeInsert(authorSid, manifest, new FileInputStream(payloadPath), payloadName); + bundle = client.rhizomeInsert(authorSid, manifest, secret, new FileInputStream(payloadPath), payloadName); System.out.println( "_status=" + bundle.status + "\n" + (bundle.rowId == null ? "" : "_rowId=" + bundle.rowId + "\n") + @@ -239,7 +251,8 @@ public class Rhizome { args[2], // manifest path args.length > 3 ? args[3] : null, // payload path args.length > 4 ? args[4] : null, // manifest out path - args.length > 5 ? args[5] : null // payload name + args.length > 5 ? args[5] : null, // payload name + args.length > 6 ? args[6] : null // bundle secret ); } catch (Exception e) { e.printStackTrace(); diff --git a/tests/rhizomejava b/tests/rhizomejava index 6f45f4ca..bb07d3ff 100755 --- a/tests/rhizomejava +++ b/tests/rhizomejava @@ -351,8 +351,7 @@ test_RhizomeInsert() { --author=${author[4]} file4 for n in 1 2 3 4; do $SED -e '/^version=/d;/^date=/d;/^filehash=/d;/^filesize=/d;/^[^a-zA-Z]/,$d' xfile$n.manifest >nmanifest$n - assertGrep nmanifest$n '^id=' - tfw_cat -v nmanifest$n + assert_manifest_fields nmanifest$n id !version !date !filehash !filesize executeJavaOk org.servalproject.test.Rhizome rhizome-insert '' nmanifest$n nfile$n nfile$n.manifest "nfile$n" tfw_cat --stdout --stderr -v nfile$n.manifest if [ -n "${author[$n]}" ]; then @@ -369,4 +368,29 @@ test_RhizomeInsert() { done } +doc_RhizomeInsertAnon="Java API update anonymous Rhizome bundle" +setup_RhizomeInsertAnon() { + setup + create_file file1 1001 + executeOk_servald rhizome add file '' file1 file1.manifest + extract_stdout_manifestid BID + extract_stdout_secret SECRET + assert_manifest_fields file1.manifest id !BK + $SED -e '/^version=/d;/^date=/d;/^filehash=/d;/^filesize=/d;/^[^a-zA-Z]/,$d' file1.manifest >file2.manifest + assert_manifest_fields file2.manifest id !version !date !filehash !filesize + create_file file2 1002 +} +test_RhizomeInsertAnon() { + executeJavaOk org.servalproject.test.Rhizome rhizome-insert '' file2.manifest file2 ifile2.manifest "file2" "$SECRET" + tfw_cat --stdout --stderr -v ifile2.manifest + assertStdoutGrep '^_status=NEW$' + assertStdoutGrep "^id=$BID\$" + assertStderrGrep --matches=1 "^bundle_status_code=NEW$CR\$" + assertStderrGrep --matches=1 --ignore-case "^bundle_status_message=.*bundle new to store.*$CR\$" + assertStderrGrep --matches=1 "^payload_status_code=NEW$CR\$" + assertStderrGrep --matches=1 --ignore-case "^payload_status_message=.*payload new to store.*$CR\$" + executeOk_servald rhizome list + assert_rhizome_list --fromhere=0 --manifest=ifile2.manifest file2 +} + runTests "$@" From e35bf77938a036b059f926e3d00d23dd3d2f027c Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Thu, 10 Jul 2014 21:19:42 +0930 Subject: [PATCH 19/21] Rhizome Java API: insert/update empty bundle --- rhizome_restful.c | 4 +++- rhizome_store.c | 14 +++++++---- tests/rhizomejava | 55 ++++++++++++++++++++++++++++++++++++++++++++ tests/rhizomerestful | 4 ++-- 4 files changed, 69 insertions(+), 8 deletions(-) diff --git a/rhizome_restful.c b/rhizome_restful.c index 8da5929e..6e83ac82 100644 --- a/rhizome_restful.c +++ b/rhizome_restful.c @@ -580,11 +580,13 @@ static int restful_rhizome_insert_end(struct http_request *hr) rhizome_manifest_set_filesize(r->manifest, r->u.insert.write.file_length); // fall through case RHIZOME_PAYLOAD_STATUS_STORED: + assert(r->manifest->filesize != RHIZOME_SIZE_UNSET); // TODO: check that stored hash matches received payload's hash // fall through case RHIZOME_PAYLOAD_STATUS_EMPTY: status_valid = 1; - assert(r->manifest->filesize != RHIZOME_SIZE_UNSET); + if (r->manifest->filesize == RHIZOME_SIZE_UNSET) + rhizome_manifest_set_filesize(r->manifest, 0); if (r->u.insert.payload_size == r->manifest->filesize) break; // fall through diff --git a/rhizome_store.c b/rhizome_store.c index 08154dd0..4967d21c 100644 --- a/rhizome_store.c +++ b/rhizome_store.c @@ -679,14 +679,18 @@ enum rhizome_payload_status rhizome_finish_write(struct rhizome_write *write) { enum rhizome_payload_status status = RHIZOME_PAYLOAD_STATUS_NEW; - // Once the whole file has been processed, we should finally know its. + // Once the whole file has been processed, we should finally know its length if (write->file_length == RHIZOME_SIZE_UNSET) { if (config.debug.rhizome_store) DEBUGF("Wrote %"PRIu64" bytes, set file_length", write->file_offset); write->file_length = write->file_offset; - status = store_make_space(write->file_length, NULL); - if (status!=RHIZOME_PAYLOAD_STATUS_NEW) - goto failure; + if (write->file_length == 0) + status = RHIZOME_PAYLOAD_STATUS_EMPTY; + else { + status = store_make_space(write->file_length, NULL); + if (status != RHIZOME_PAYLOAD_STATUS_NEW) + goto failure; + } } // flush out any remaining buffered pieces to disk @@ -709,7 +713,7 @@ enum rhizome_payload_status rhizome_finish_write(struct rhizome_write *write) } assert(write->file_offset == write->file_length); - if (write->file_length==0){ + if (write->file_length == 0) { // whoops, no payload, don't store anything if (config.debug.rhizome_store) DEBUGF("Ignoring empty write"); diff --git a/tests/rhizomejava b/tests/rhizomejava index bb07d3ff..7e3f4f24 100755 --- a/tests/rhizomejava +++ b/tests/rhizomejava @@ -393,4 +393,59 @@ test_RhizomeInsertAnon() { assert_rhizome_list --fromhere=0 --manifest=ifile2.manifest file2 } +doc_RhizomeInsertEmptyNew="Java API update existing Rhizome bundle to empty" +setup_RhizomeInsertEmptyNew() { + setup + >empty + assert [ ! -s empty ] +} +test_RhizomeInsertEmptyNew() { + executeJavaOk org.servalproject.test.Rhizome rhizome-insert '' '' empty empty.manifest "lucky" + tfw_cat --stdout --stderr -v empty.manifest + extract_manifest_id BID empty.manifest + assertStdoutGrep '^_status=NEW$' + assertStdoutGrep "^id=$BID\$" + assertStdoutGrep "^filesize=0\$" + assertStdoutGrep --matches=0 "^filehash=" + assertStderrGrep --matches=1 "^bundle_status_code=NEW$CR\$" + assertStderrGrep --matches=1 --ignore-case "^bundle_status_message=.*bundle new to store.*$CR\$" + assertStderrGrep --matches=1 "^payload_status_code=EMPTY$CR\$" + assertStderrGrep --matches=1 --ignore-case "^payload_status_message=.*payload empty.*$CR\$" + executeOk_servald rhizome list + assert_rhizome_list empty + executeOk_servald rhizome extract bundle "$BID" xempty.manifest xempty + assert [ ! -e xempty ] + assert diff xempty.manifest empty.manifest +} + +doc_RhizomeInsertEmptyUpdate="Java API insert new empty Rhizome bundle" +setup_RhizomeInsertEmptyUpdate() { + setup + create_file file1 1001 + executeOk_servald rhizome add file "$SIDA1" file1 file1.manifest + extract_stdout_manifestid BID + extract_stdout_BK BK + >empty + assert [ ! -s empty ] + echo "id=$BID" >iempty.manifest + echo "BK=$BK" >>iempty.manifest +} +test_RhizomeInsertEmptyUpdate() { + executeJavaOk org.servalproject.test.Rhizome rhizome-insert '' iempty.manifest empty empty.manifest + tfw_cat --stdout --stderr -v empty.manifest + assertStdoutGrep '^_status=NEW$' + assertStdoutGrep "^id=$BID\$" + assertStdoutGrep "^filesize=0\$" + assertStdoutGrep --matches=0 "^filehash=" + assertStderrGrep --matches=1 "^bundle_status_code=NEW$CR\$" + assertStderrGrep --matches=1 --ignore-case "^bundle_status_message=.*bundle new to store.*$CR\$" + assertStderrGrep --matches=1 "^payload_status_code=EMPTY$CR\$" + assertStderrGrep --matches=1 --ignore-case "^payload_status_message=.*payload empty.*$CR\$" + executeOk_servald rhizome list + assert_rhizome_list empty + executeOk_servald rhizome extract bundle "$BID" xempty.manifest xempty + assert [ ! -e xempty ] + assert diff xempty.manifest empty.manifest +} + runTests "$@" diff --git a/tests/rhizomerestful b/tests/rhizomerestful index 8fe06aa2..c5e895cd 100755 --- a/tests/rhizomerestful +++ b/tests/rhizomerestful @@ -650,8 +650,8 @@ test_RhizomeInsertEmpty() { assertStdoutIs 201 assertGrep --matches=1 --ignore-case http.header "^Serval-Rhizome-Result-Bundle-Status-Code: 0$CR\$" assertGrep --matches=1 --ignore-case http.header "^Serval-Rhizome-Result-Bundle-Status-Message: .*bundle new to store.*$CR\$" - assertGrep --matches=1 --ignore-case http.header "^Serval-Rhizome-Result-Payload-Status-Code: 1$CR\$" - assertGrep --matches=1 --ignore-case http.header "^Serval-Rhizome-Result-Payload-Status-Message: .*payload new to store.*$CR\$" + assertGrep --matches=1 --ignore-case http.header "^Serval-Rhizome-Result-Payload-Status-Code: 0$CR\$" + assertGrep --matches=1 --ignore-case http.header "^Serval-Rhizome-Result-Payload-Status-Message: .*payload empty.*$CR\$" extract_manifest_id BID empty.manifest executeOk_servald rhizome list assert_rhizome_list empty From 8842f32b193876723dce5807fcaadd791ed75a5e Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Thu, 10 Jul 2014 22:23:11 +0930 Subject: [PATCH 20/21] Rhizome Java API: insert journal is not implemented Change the HTTP response code for unimplemented operations from 403 to 501 --- .../ServalDNotImplementedException.java | 35 +++++++++++++++++++ .../servaldna/rhizome/RhizomeCommon.java | 10 ++++-- java/org/servalproject/test/Rhizome.java | 4 +++ rhizome_restful.c | 19 ++++++---- tests/rhizomejava | 15 ++++++++ tests/rhizomerestful | 4 +-- 6 files changed, 75 insertions(+), 12 deletions(-) create mode 100644 java/org/servalproject/servaldna/ServalDNotImplementedException.java diff --git a/java/org/servalproject/servaldna/ServalDNotImplementedException.java b/java/org/servalproject/servaldna/ServalDNotImplementedException.java new file mode 100644 index 00000000..5cced259 --- /dev/null +++ b/java/org/servalproject/servaldna/ServalDNotImplementedException.java @@ -0,0 +1,35 @@ +/** + * 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; + +/** + * Thrown when the Serval DNA interface is used to perform an operation that is not yet implemented, + * but will be provided in future. + * + * @author Andrew Bettison + */ +public class ServalDNotImplementedException extends ServalDInterfaceException +{ + public ServalDNotImplementedException(String message) { + super(message); + } + +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java index 2341feca..bec9bb81 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java @@ -44,6 +44,7 @@ import org.servalproject.servaldna.BundleSecret; import org.servalproject.servaldna.ServalDHttpConnectionFactory; import org.servalproject.servaldna.ServalDInterfaceException; import org.servalproject.servaldna.ServalDFailureException; +import org.servalproject.servaldna.ServalDNotImplementedException; public class RhizomeCommon { @@ -88,12 +89,15 @@ public class RhizomeCommon } if (!conn.getContentType().equals("application/json")) throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType()); - if (conn.getResponseCode() == HttpURLConnection.HTTP_FORBIDDEN) { + if (status.http_status_code >= 300) { JSONTokeniser json = new JSONTokeniser(new InputStreamReader(conn.getErrorStream(), "US-ASCII")); decodeRestfulStatus(status, json); - return status; } - throw new ServalDInterfaceException("unexpected HTTP response code: " + conn.getResponseCode()); + 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(Status status) diff --git a/java/org/servalproject/test/Rhizome.java b/java/org/servalproject/test/Rhizome.java index dc5603ca..9c713333 100644 --- a/java/org/servalproject/test/Rhizome.java +++ b/java/org/servalproject/test/Rhizome.java @@ -28,6 +28,7 @@ import java.io.FileOutputStream; import java.io.IOException; import org.servalproject.servaldna.ServalDClient; import org.servalproject.servaldna.ServalDInterfaceException; +import org.servalproject.servaldna.ServalDNotImplementedException; import org.servalproject.servaldna.ServerControl; import org.servalproject.servaldna.BundleId; import org.servalproject.servaldna.BundleSecret; @@ -229,6 +230,9 @@ public class Rhizome { catch (RhizomeException e) { System.out.println(e.toString()); } + catch (ServalDNotImplementedException e) { + System.out.println(e.toString()); + } System.exit(0); } diff --git a/rhizome_restful.c b/rhizome_restful.c index 6e83ac82..1894f682 100644 --- a/rhizome_restful.c +++ b/rhizome_restful.c @@ -536,7 +536,7 @@ static int insert_mime_part_end(struct http_request *hr) return 500; } if (r->manifest->is_journal) - return http_request_rhizome_response(r, 403, "Insert not supported for journals", NULL); + return http_request_rhizome_response(r, 501, "Insert not supported for journals", NULL); assert(r->manifest != NULL); } else if (r->u.insert.current_part == PART_PAYLOAD) { @@ -892,10 +892,13 @@ static void render_manifest_headers(struct http_request *hr, strbuf sb) } rhizome_manifest *m = r->manifest; if (m) { - strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Id: %s\r\n", alloca_tohex_rhizome_bid_t(m->cryptoSignPublic)); - strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Version: %"PRIu64"\r\n", m->version); - strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Filesize: %"PRIu64"\r\n", m->filesize); - if (m->filesize != 0) + if (m->has_id) + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Id: %s\r\n", alloca_tohex_rhizome_bid_t(m->cryptoSignPublic)); + if (m->version) + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Version: %"PRIu64"\r\n", m->version); + if (m->filesize != RHIZOME_SIZE_UNSET) + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Filesize: %"PRIu64"\r\n", m->filesize); + if (m->has_filehash) strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Filehash: %s\r\n", alloca_tohex_rhizome_filehash_t(m->filehash)); if (m->has_sender) strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Sender: %s\r\n", alloca_tohex_sid_t(m->sender)); @@ -932,7 +935,9 @@ static void render_manifest_headers(struct http_request *hr, strbuf sb) rhizome_bytes_to_hex_upper(m->cryptoSignSecret, secret, RHIZOME_BUNDLE_KEY_BYTES); strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Secret: %s\r\n", secret); } - strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Rowid: %"PRIu64"\r\n", m->rowid); - strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Inserttime: %"PRIu64"\r\n", m->inserttime); + if (m->rowid) + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Rowid: %"PRIu64"\r\n", m->rowid); + if (m->inserttime) + strbuf_sprintf(sb, "Serval-Rhizome-Bundle-Inserttime: %"PRIu64"\r\n", m->inserttime); } } diff --git a/tests/rhizomejava b/tests/rhizomejava index 7e3f4f24..eab8b697 100755 --- a/tests/rhizomejava +++ b/tests/rhizomejava @@ -448,4 +448,19 @@ test_RhizomeInsertEmptyUpdate() { assert diff xempty.manifest empty.manifest } +doc_RhizomeInsertJournal="Java API insert Rhizome bundle does not support journals" +setup_RhizomeInsertJournal() { + setup + echo 'File one' >file1 + echo 'tail=0' >file1.manifest +} +test_RhizomeInsertJournal() { + executeJavaOk org.servalproject.test.Rhizome rhizome-insert '' file1.manifest file1 ifile1.manifest + tfw_cat --stdout --stderr + assertStdoutGrep ServalDNotImplementedException + assertStdoutGrep --ignore-case "not supported.*journal" + executeOk_servald rhizome list + assert_rhizome_list +} + runTests "$@" diff --git a/tests/rhizomerestful b/tests/rhizomerestful index c5e895cd..9aab4898 100755 --- a/tests/rhizomerestful +++ b/tests/rhizomerestful @@ -800,8 +800,8 @@ test_RhizomeInsertJournal() { "http://$addr_localhost:$PORTA/restful/rhizome/insert" tfw_cat http.header http.body assertExitStatus == 0 - assertStdoutIs 403 - assertJq http.body 'contains({"http_status_code": 403})' + assertStdoutIs 501 + assertJq http.body 'contains({"http_status_code": 501})' assertJqGrep --ignore-case http.body '.http_status_message' 'not supported.*journal' executeOk_servald rhizome list assert_rhizome_list From 2757a08b87d3614f48f6e7c528280de25332dbff Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Fri, 11 Jul 2014 11:14:15 +0930 Subject: [PATCH 21/21] Rhizome Java API: insert inconsistent bundle --- .../servaldna/rhizome/RhizomeCommon.java | 38 ++++++++++--------- .../rhizome/RhizomeEncryptionException.java | 4 +- .../rhizome/RhizomeFakeManifestException.java | 4 +- .../RhizomeInconsistencyException.java | 4 +- .../RhizomeInvalidManifestException.java | 4 +- .../rhizome/RhizomeReadOnlyException.java | 4 +- tests/rhizomejava | 34 +++++++++++++++++ 7 files changed, 65 insertions(+), 27 deletions(-) diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java index bec9bb81..04abfca9 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java @@ -100,14 +100,15 @@ public class RhizomeCommon throw new ServalDInterfaceException("unexpected HTTP response: " + status.http_status_code + " " + status.http_status_message); } - protected static ServalDInterfaceException unexpectedResponse(Status status) + protected static ServalDInterfaceException unexpectedResponse(HttpURLConnection conn, Status status) { return new ServalDInterfaceException( - "unexpected Rhizome failure, \"" + status.http_status_message + "\"" + "unexpected Rhizome failure, " + quoteString(status.http_status_message) + (status.bundle_status_code == null ? "" : ", " + status.bundle_status_code) - + (status.bundle_status_message == null ? "" : " \"" + status.bundle_status_message + "\"") + + (status.bundle_status_message == null ? "" : " " + quoteString(status.bundle_status_message)) + (status.payload_status_code == null ? "" : ", " + status.payload_status_code) - + (status.payload_status_message == null ? "" : ", " + status.payload_status_message + "\"") + + (status.payload_status_message == null ? "" : " " + quoteString(status.payload_status_message)) + + " from " + conn.getURL() ); } @@ -236,7 +237,7 @@ public class RhizomeCommon if (status.input_stream != null) status.input_stream.close(); } - throw unexpectedResponse(status); + throw unexpectedResponse(conn, status); } public static RhizomePayloadRawBundle rhizomePayloadRaw(ServalDHttpConnectionFactory connector, BundleId bid) @@ -285,7 +286,7 @@ public class RhizomeCommon if (status.input_stream != null) status.input_stream.close(); } - throw unexpectedResponse(status); + throw unexpectedResponse(conn, status); } public static RhizomePayloadBundle rhizomePayload(ServalDHttpConnectionFactory connector, BundleId bid) @@ -336,7 +337,7 @@ public class RhizomeCommon if (status.input_stream != null) status.input_stream.close(); } - throw unexpectedResponse(status); + throw unexpectedResponse(conn, status); } public static RhizomeInsertBundle rhizomeInsert(ServalDHttpConnectionFactory connector, @@ -376,6 +377,7 @@ public class RhizomeCommon conn.connect(); OutputStream ost = conn.getOutputStream(); PrintStream wr = new PrintStream(ost, false, "US-ASCII"); + wr.print(new Object(){}.getClass().getEnclosingClass().getName()); if (author != null) { wr.print("\r\n--" + boundary + "\r\n"); wr.print("Content-Disposition: form-data; name=\"bundle-author\"\r\n"); @@ -426,7 +428,7 @@ public class RhizomeCommon switch (status.payload_status_code) { case ERROR: dumpStatus(status, System.err); - throw new ServalDFailureException("received rhizome_payload_status_code=ERROR(-1) from " + conn.getURL()); + throw new ServalDFailureException("received Rhizome payload_status=ERROR " + quoteString(status.payload_status_message) + " from " + conn.getURL()); case EMPTY: case NEW: case STORED: @@ -434,26 +436,26 @@ public class RhizomeCommon dumpStatus(status, System.err); switch (status.bundle_status_code) { case ERROR: - throw new ServalDFailureException("received rhizome_bundle_status_code=ERROR(-1) from " + conn.getURL()); + throw new ServalDFailureException("received Rhizome bundle_status=ERROR " + quoteString(status.bundle_status_message) + " from " + conn.getURL()); case NEW: case SAME: case DUPLICATE: case OLD: case NO_ROOM: { if (!conn.getContentType().equals("rhizome-manifest/text")) - throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType()); + throw new ServalDInterfaceException("unexpected HTTP Content-Type " + conn.getContentType() + " from " + conn.getURL()); RhizomeManifest returned_manifest = RhizomeManifest.fromTextFormat(status.input_stream); BundleExtra extra = bundleExtraFromHeaders(conn); return new RhizomeInsertBundle(status.bundle_status_code, returned_manifest, extra.rowId, extra.insertTime, extra.author, extra.secret); } case INVALID: - throw new RhizomeInvalidManifestException(conn.getURL()); + throw new RhizomeInvalidManifestException(status.bundle_status_message, conn.getURL()); case FAKE: - throw new RhizomeFakeManifestException(conn.getURL()); + throw new RhizomeFakeManifestException(status.bundle_status_message, conn.getURL()); case INCONSISTENT: - throw new RhizomeInconsistencyException(conn.getURL()); + throw new RhizomeInconsistencyException(status.bundle_status_message, conn.getURL()); case READONLY: - throw new RhizomeReadOnlyException(conn.getURL()); + throw new RhizomeReadOnlyException(status.bundle_status_message, conn.getURL()); } break; case TOO_BIG: @@ -463,10 +465,10 @@ public class RhizomeCommon case WRONG_SIZE: case WRONG_HASH: dumpStatus(status, System.err); - throw new RhizomeInconsistencyException(conn.getURL()); + throw new RhizomeInconsistencyException(status.payload_status_message, conn.getURL()); case CRYPTO_FAIL: dumpStatus(status, System.err); - throw new RhizomeEncryptionException(conn.getURL()); + throw new RhizomeEncryptionException(status.payload_status_message, conn.getURL()); } } catch (RhizomeManifestParseException e) { @@ -477,7 +479,7 @@ public class RhizomeCommon status.input_stream.close(); } dumpStatus(status, System.err); - throw unexpectedResponse(status); + throw unexpectedResponse(conn, status); } private static void dumpHeaders(HttpURLConnection conn, PrintStream out) @@ -523,6 +525,8 @@ public class RhizomeCommon 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) { diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeEncryptionException.java b/java/org/servalproject/servaldna/rhizome/RhizomeEncryptionException.java index d91b1ac6..a9688c86 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeEncryptionException.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeEncryptionException.java @@ -31,8 +31,8 @@ import java.net.URL; */ public class RhizomeEncryptionException extends RhizomeException { - public RhizomeEncryptionException(URL url) { - super("cannot encrypt payload", url); + public RhizomeEncryptionException(String message, URL url) { + super(message == null ? "cannot encrypt payload" : message, url); } } diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeFakeManifestException.java b/java/org/servalproject/servaldna/rhizome/RhizomeFakeManifestException.java index 7bf44c67..06a2463e 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeFakeManifestException.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeFakeManifestException.java @@ -29,8 +29,8 @@ import java.net.URL; */ public class RhizomeFakeManifestException extends RhizomeException { - public RhizomeFakeManifestException(URL url) { - super("unsigned manifest", url); + public RhizomeFakeManifestException(String message, URL url) { + super(message == null ? "unsigned manifest" : message, url); } } diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeInconsistencyException.java b/java/org/servalproject/servaldna/rhizome/RhizomeInconsistencyException.java index cdab7985..d3520a07 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeInconsistencyException.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeInconsistencyException.java @@ -30,8 +30,8 @@ import java.net.URL; */ public class RhizomeInconsistencyException extends RhizomeException { - public RhizomeInconsistencyException(URL url) { - super("manifest inconsistent with payload", url); + public RhizomeInconsistencyException(String message, URL url) { + super(message == null ? "manifest inconsistent with payload" : message, url); } } diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeInvalidManifestException.java b/java/org/servalproject/servaldna/rhizome/RhizomeInvalidManifestException.java index f1966218..0e6586d9 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeInvalidManifestException.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeInvalidManifestException.java @@ -31,8 +31,8 @@ import java.net.URL; */ public class RhizomeInvalidManifestException extends RhizomeException { - public RhizomeInvalidManifestException(URL url) { - super("invalid manifest", url); + public RhizomeInvalidManifestException(String message, URL url) { + super(message == null ? "invalid manifest" : message, url); } public RhizomeInvalidManifestException(RhizomeIncompleteManifest manifest) { diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeReadOnlyException.java b/java/org/servalproject/servaldna/rhizome/RhizomeReadOnlyException.java index d910c77c..65f26411 100644 --- a/java/org/servalproject/servaldna/rhizome/RhizomeReadOnlyException.java +++ b/java/org/servalproject/servaldna/rhizome/RhizomeReadOnlyException.java @@ -30,8 +30,8 @@ import java.net.URL; */ public class RhizomeReadOnlyException extends RhizomeException { - public RhizomeReadOnlyException(URL url) { - super("bundle cannot be modified", url); + public RhizomeReadOnlyException(String message, URL url) { + super(message == null ? "bundle cannot be modified" : message, url); } } diff --git a/tests/rhizomejava b/tests/rhizomejava index eab8b697..8016f03f 100755 --- a/tests/rhizomejava +++ b/tests/rhizomejava @@ -463,4 +463,38 @@ test_RhizomeInsertJournal() { assert_rhizome_list } +doc_RhizomeInsertIncorrectFilesize="Java API insert Rhizome bundle, incorrect filesize" +setup_RhizomeInsertIncorrectFilesize() { + setup + echo 'File one' >file1 + echo 'filesize=6' >file1.manifest + echo 'File two' >file2 + echo 'filesize=100' >file2.manifest +} +test_RhizomeInsertIncorrectFilesize() { + for n in 1 2; do + executeJavaOk org.servalproject.test.Rhizome rhizome-insert '' file$n.manifest file$n ifile$n.manifest + tfw_cat --stdout --stderr + assertStdoutGrep RhizomeInconsistencyException + assertStdoutGrep --ignore-case 'payload size.*contradicts manifest' + done + executeOk_servald rhizome list + assert_rhizome_list +} + +doc_RhizomeInsertIncorrectFilehash="Java API insert Rhizome bundle, incorrect filehash" +setup_RhizomeInsertIncorrectFilehash() { + setup + echo 'File one' >file1 + echo 'filehash=FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' >file1.manifest +} +test_RhizomeInsertIncorrectFilehash() { + executeJavaOk org.servalproject.test.Rhizome rhizome-insert '' file1.manifest file1 ifile1.manifest + tfw_cat --stdout --stderr + assertStdoutGrep RhizomeInconsistencyException + assertStdoutGrep --ignore-case 'payload hash.*contradicts manifest' + executeOk_servald rhizome list + assert_rhizome_list +} + runTests "$@"