diff options
Diffstat (limited to 'test/s3comms.c')
-rw-r--r-- | test/s3comms.c | 2643 |
1 files changed, 2643 insertions, 0 deletions
diff --git a/test/s3comms.c b/test/s3comms.c new file mode 100644 index 0000000..239e3ed --- /dev/null +++ b/test/s3comms.c @@ -0,0 +1,2643 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Copyright by The HDF Group. * + * All rights reserved. * + * * + * This file is part of HDF5. The full HDF5 copyright notice, including * + * terms governing use, modification, and redistribution, is contained in * + * the COPYING file, which can be found at the root of the source code * + * distribution tree, or in https://www.hdfgroup.org/licenses. * + * If you do not have access to either file, you may request a copy from * + * help@hdfgroup.org. * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +/* + * Read-Only S3 Virtual File Driver (VFD) + * + * Purpose: Unit tests for the S3 Communications (s3comms) module. + * + * Programmer: Jacob Smith + * 2017-10-11 + */ + +#include "h5test.h" +#include "H5FDs3comms.h" +#include "H5MMprivate.h" /* memory management */ + +#ifdef H5_HAVE_ROS3_VFD + +/***************************************************************************** + * + * FILE-LOCAL TESTING MACROS + * + * Purpose: + * + * 1) Upon test failure, goto-jump to single-location teardown in test + * function. E.g., `error:` (consistency with HDF corpus) or + * `failed:` (reflects purpose). + * >>> using "error", in part because `H5E_BEGIN_TRY` expects it. + * 2) Increase clarity and reduce overhead found with `TEST_ERROR`. + * e.g., "if(somefunction(arg, arg2) < 0) TEST_ERROR:" + * requires reading of entire line to know whether this if/call is + * part of the test setup, test operation, or a test unto itself. + * 3) Provide testing macros with optional user-supplied failure message; + * if not supplied (NULL), generate comparison output in the spirit of + * test-driven development. E.g., "expected 5 but was -3" + * User messages clarify test's purpose in code, encouraging description + * without relying on comments. + * 4) Configurable expected-actual order in generated comparison strings. + * Some prefer `VERIFY(expected, actual)`, others + * `VERIFY(actual, expected)`. Provide preprocessor ifdef switch + * to satifsy both parties, assuming one paradigm per test file. + * (One could #undef and redefine the flag through the file as desired, + * but _why_.) + * Provided as courtesy, per consideration for inclusion in the library + * proper. + * + * Macros: + * + * JSVERIFY_EXP_ACT - ifdef flag, configures comparison order + * FAIL_IF() - check condition + * FAIL_UNLESS() - check _not_ condition + * JSVERIFY() - long-int equality check; prints reason/comparison + * JSVERIFY_NOT() - long-int inequality check; prints + * JSVERIFY_STR() - string equality check; prints + * + * Programmer: Jacob Smith + * 2017-10-24 + * + *****************************************************************************/ + +/*---------------------------------------------------------------------------- + * + * ifdef flag: JSVERIFY_EXP_ACT + * + * JSVERIFY macros accept arguments as (EXPECTED, ACTUAL[, reason]) + * default, if this is undefined, is (ACTUAL, EXPECTED[, reason]) + * + *---------------------------------------------------------------------------- + */ +#define JSVERIFY_EXP_ACT 1L + +/*---------------------------------------------------------------------------- + * + * Macro: JSFAILED_AT() + * + * Purpose: + * + * Preface a test failure by printing "*FAILED*" and location to stdout + * Similar to `H5_FAILED(); AT();` from h5test.h + * + * *FAILED* at somefile.c:12 in function_name()... + * + * Programmer: Jacob Smith + * 2017-10-24 + * + *---------------------------------------------------------------------------- + */ +#define JSFAILED_AT() \ + { \ + HDprintf("*FAILED* at %s:%d in %s()...\n", __FILE__, __LINE__, FUNC); \ + } + +/*---------------------------------------------------------------------------- + * + * Macro: FAIL_IF() + * + * Purpose: + * + * Make tests more accessible and less cluttered than + * `if (thing == otherthing()) TEST_ERROR` + * paradigm. + * + * The following lines are roughly equivalent: + * + * `if (myfunc() < 0) TEST_ERROR;` (as seen elsewhere in HDF tests) + * `FAIL_IF(myfunc() < 0)` + * + * Prints a generic "FAILED AT" line to stdout and jumps to `error`, + * similar to `TEST_ERROR` in h5test.h + * + * Programmer: Jacob Smith + * 2017-10-23 + * + *---------------------------------------------------------------------------- + */ +#define FAIL_IF(condition) \ + if (condition) { \ + JSFAILED_AT() \ + goto error; \ + } + +/*---------------------------------------------------------------------------- + * + * Macro: FAIL_UNLESS() + * + * Purpose: + * + * TEST_ERROR wrapper to reduce cognitive overhead from "negative tests", + * e.g., "a != b". + * + * Opposite of FAIL_IF; fails if the given condition is _not_ true. + * + * `FAIL_IF( 5 != my_op() )` + * is equivalent to + * `FAIL_UNLESS( 5 == my_op() )` + * However, `JSVERIFY(5, my_op(), "bad return")` may be even clearer. + * (see JSVERIFY) + * + * Programmer: Jacob Smith + * 2017-10-24 + * + *---------------------------------------------------------------------------- + */ +#define FAIL_UNLESS(condition) \ + if (!(condition)) { \ + JSFAILED_AT() \ + goto error; \ + } + +/*---------------------------------------------------------------------------- + * + * Macro: JSERR_LONG() + * + * Purpose: + * + * Print an failure message for long-int arguments. + * ERROR-AT printed first. + * If `reason` is given, it is printed on own line and newlined after + * else, prints "expected/actual" aligned on own lines. + * + * *FAILED* at myfile.c:488 in somefunc()... + * forest must be made of trees. + * + * or + * + * *FAILED* at myfile.c:488 in somefunc()... + * ! Expected 425 + * ! Actual 3 + * + * Programmer: Jacob Smith + * 2017-10-24 + * + *---------------------------------------------------------------------------- + */ +#define JSERR_LONG(expected, actual, reason) \ + { \ + JSFAILED_AT() \ + if (reason != NULL) { \ + HDprintf("%s\n", (reason)); \ + } \ + else { \ + HDprintf(" ! Expected %ld\n ! Actual %ld\n", (long)(expected), (long)(actual)); \ + } \ + } + +/*---------------------------------------------------------------------------- + * + * Macro: JSERR_STR() + * + * Purpose: + * + * Print an failure message for string arguments. + * ERROR-AT printed first. + * If `reason` is given, it is printed on own line and newlined after + * else, prints "expected/actual" aligned on own lines. + * + * *FAILED* at myfile.c:421 in myfunc()... + * Blue and Red strings don't match! + * + * or + * + * *FAILED* at myfile.c:421 in myfunc()... + * !!! Expected: + * this is my expected + * string + * !!! Actual: + * not what I expected at all + * + * Programmer: Jacob Smith + * 2017-10-24 + * + *---------------------------------------------------------------------------- + */ +#define JSERR_STR(expected, actual, reason) \ + { \ + JSFAILED_AT() \ + if ((reason) != NULL) { \ + HDprintf("%s\n", (reason)); \ + } \ + else { \ + HDprintf("!!! Expected:\n%s\n!!!Actual:\n%s\n", (expected), (actual)); \ + } \ + } + +#ifdef JSVERIFY_EXP_ACT +/* VERIFY rountines with paramter order (<expected>, <actual> [, <msg> ]) + */ + +/*---------------------------------------------------------------------------- + * + * Macro: JSVERIFY() + * + * Purpose: + * + * Verify that two long integers are equal. + * If unequal, print failure message + * (with `reason`, if not NULL; expected/actual if NULL) + * and jump to `error` at end of function + * + * Programmer: Jacob Smith + * 2017-10-24 + * + *---------------------------------------------------------------------------- + */ +#define JSVERIFY(expected, actual, reason) \ + if ((long)(actual) != (long)(expected)) { \ + JSERR_LONG((expected), (actual), (reason)) \ + goto error; \ + } /* JSVERIFY */ + +#if 0 /* UNUSED */ + +/*---------------------------------------------------------------------------- + * + * Macro: JSVERIFY_NOT() + * + * Purpose: + * + * Verify that two long integers are _not_ equal. + * If equal, print failure message + * (with `reason`, if not NULL; expected/actual if NULL) + * and jump to `error` at end of function + * + * Programmer: Jacob Smith + * 2017-10-24 + * + *---------------------------------------------------------------------------- + */ +#define JSVERIFY_NOT(expected, actual, reason) \ + if ((long)(actual) == (long)(expected)) { \ + JSERR_LONG((expected), (actual), (reason)) \ + goto error; \ + } /* JSVERIFY_NOT */ +#endif /* JSVERIFY_NOT unused */ + +/*---------------------------------------------------------------------------- + * + * Macro: JSVERIFY_STR() + * + * Purpose: + * + * Verify that two strings are equal. + * If unequal, print failure message + * (with `reason`, if not NULL; expected/actual if NULL) + * and jump to `error` at end of function + * + * Programmer: Jacob Smith + * 2017-10-24 + * + *---------------------------------------------------------------------------- + */ +#define JSVERIFY_STR(expected, actual, reason) \ + if (strcmp((actual), (expected)) != 0) { \ + JSERR_STR((expected), (actual), (reason)); \ + goto error; \ + } /* JSVERIFY_STR */ + +#else +/* JSVERIFY_EXP_ACT not defined + * + * Repeats macros above, but with actual/expected parameters reversed. + */ + +/*---------------------------------------------------------------------------- + * Macro: JSVERIFY() + * See: JSVERIFY documentation above. + * Programmer: Jacob Smith + * 2017-10-14 + *---------------------------------------------------------------------------- + */ +#define JSVERIFY(actual, expected, reason) \ + if ((long)(actual) != (long)(expected)) { \ + JSERR_LONG((expected), (actual), (reason)); \ + goto error; \ + } /* JSVERIFY */ + +#if 0 /* UNUSED */ + +/*---------------------------------------------------------------------------- + * Macro: JSVERIFY_NOT() + * See: JSVERIFY_NOT documentation above. + * Programmer: Jacob Smith + * 2017-10-14 + *---------------------------------------------------------------------------- + */ +#define JSVERIFY_NOT(actual, expected, reason) \ + if ((long)(actual) == (long)(expected)) { \ + JSERR_LONG((expected), (actual), (reason)) \ + goto error; \ + } /* JSVERIFY_NOT */ +#endif /* JSVERIFY_NOT unused */ + +/*---------------------------------------------------------------------------- + * Macro: JSVERIFY_STR() + * See: JSVERIFY_STR documentation above. + * Programmer: Jacob Smith + * 2017-10-14 + *---------------------------------------------------------------------------- + */ +#define JSVERIFY_STR(actual, expected, reason) \ + if (strcmp((actual), (expected)) != 0) { \ + JSERR_STR((expected), (actual), (reason)); \ + goto error; \ + } /* JSVERIFY_STR */ + +#endif /* ifdef/else JSVERIFY_EXP_ACT */ + +#define S3_TEST_PROFILE_NAME "ros3_vfd_test" + +#define S3_TEST_RESOURCE_TEXT_RESTRICTED "t8.shakespeare.txt" +#define S3_TEST_RESOURCE_TEXT_PUBLIC "Poe_Raven.txt" +#define S3_TEST_RESOURCE_MISSING "missing.csv" + +#define S3_TEST_RUN_TIMEOUT 0 /* run tests that might hang */ +#define S3_TEST_MAX_URL_SIZE 256 /* char array size */ + +/* Global variables for aws test profile. + * An attempt is made to read ~/.aws/credentials and ~/.aws/config upon test + * startup -- if unable to open either file or cannot load region, id, and key, + * tests connecting with S3 will not be run + */ +static int s3_test_credentials_loaded = 0; +static char s3_test_aws_region[16] = ""; +static char s3_test_aws_access_key_id[64] = ""; +static char s3_test_aws_secret_access_key[128] = ""; +static char s3_test_bucket_url[S3_TEST_MAX_URL_SIZE] = ""; +static hbool_t s3_test_bucket_defined = FALSE; + +/*--------------------------------------------------------------------------- + * + * Function: test_macro_format_credential() + * + * Purpose: + * + * Demonstrate that the macro `S3COMMS_FORMAT_CREDENTIAL` + * performs as expected. + * + * Programmer: Jacob Smith + * 2017-09-19 + * + *---------------------------------------------------------------------------- + */ +static herr_t +test_macro_format_credential(void) +{ + /************************ + * test-local variables * + ************************/ + + char dest[256]; + const char access[] = "AKIAIOSFODNN7EXAMPLE"; + const char date[] = "20130524"; + const char region[] = "us-east-1"; + const char service[] = "s3"; + const char expected[] = "AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request"; + + TESTING("test_macro_format_credential"); + + FAIL_IF(S3COMMS_MAX_CREDENTIAL_SIZE < S3COMMS_FORMAT_CREDENTIAL(dest, access, date, region, service)) + + JSVERIFY_STR(expected, dest, NULL) + + PASSED(); + return 0; + +error: + return -1; + +} /* end test_macro_format_credential() */ + +/*--------------------------------------------------------------------------- + * + * Function: test_aws_canonical_request() + * + * Purpose: + * + * Demonstrate the construction of a Canoncial Request (and Signed Headers) + * + * Elided / not yet implemented: + * Query strings + * request "body" + * + * Programmer: Jacob Smith + * 2017-10-04 + * + *--------------------------------------------------------------------------- + */ +static herr_t +test_aws_canonical_request(void) +{ + /************************* + * test-local structures * + *************************/ + + struct header { + const char *name; + const char *value; + }; + + struct testcase { + const char * exp_request; + const char * exp_headers; + const char * verb; + const char * resource; + unsigned int listsize; + struct header list[5]; + }; + + /************************ + * test-local variables * + ************************/ + + struct testcase cases[] = { + { + "GET\n/some/" + "path.file\n\nhost:somebucket.someserver.somedomain\nrange:bytes=150-244\n\nhost;" + "range\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "host;range", + "GET", + "/some/path.file", + 2, + { + {"Range", "bytes=150-244"}, + {"Host", "somebucket.someserver.somedomain"}, + }, + }, + {"HEAD\n/bucketpath/" + "myfile.dat\n\nhost:place.domain\nx-amz-content-sha256:" + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:" + "19411207T150803Z\n\nhost;x-amz-content-sha256;x-amz-" + "date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "host;x-amz-content-sha256;x-amz-date", + "HEAD", + "/bucketpath/myfile.dat", + 3, + { + {"x-amz-content-sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, + {"host", "place.domain"}, + {"x-amz-date", "19411207T150803Z"}, + }}, + { + "PUT\n/\n\n\n\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "", + "PUT", + "/", + 0, + { + {"", ""}, + }, /* unused; satisfies compiler */ + }, + }; /* struct testcase cases[] */ + struct testcase *C = NULL; + char cr_dest[512]; /* canonical request */ + hrb_t * hrb = NULL; /* http request buffer object */ + unsigned int i = 0; /* looping/indexing */ + unsigned int j = 0; /* looping/indexing */ + hrb_node_t * node = NULL; /* http headers list pointer */ + unsigned int n_cases = 3; + char sh_dest[64]; /* signed headers */ + + TESTING("test_aws_canonical_request"); + + for (i = 0; i < n_cases; i++) { + /* pre-test bookkeeping + */ + C = &cases[i]; + for (j = 0; j < 256; j++) { + cr_dest[j] = 0; + } /* zero request buffer */ + for (j = 0; j < 64; j++) { + sh_dest[j] = 0; + } /* zero headers buffer */ + + /* create HTTP request object with given verb, resource/path + */ + hrb = H5FD_s3comms_hrb_init_request(C->verb, C->resource, "HTTP/1.1"); + HDassert(hrb->body == NULL); + + /* Create headers list from test case input + */ + for (j = 0; j < C->listsize; j++) { + FAIL_IF(FAIL == H5FD_s3comms_hrb_node_set(&node, C->list[j].name, C->list[j].value)); + } + + hrb->first_header = node; + + /* test + */ + JSVERIFY(SUCCEED, H5FD_s3comms_aws_canonical_request(cr_dest, 512, sh_dest, 64, hrb), + " unable to compose canonical request") + JSVERIFY_STR(C->exp_headers, sh_dest, NULL) + JSVERIFY_STR(C->exp_request, cr_dest, NULL) + + /* tear-down + */ + while (node != NULL) { + FAIL_IF(FAIL == H5FD_s3comms_hrb_node_set(&node, node->name, NULL)); + } + HDassert(NULL == node); + FAIL_IF(FAIL == H5FD_s3comms_hrb_destroy(&hrb)); + HDassert(NULL == hrb); + + } /* for each test case */ + + /*************** + * ERROR CASES * + ***************/ + + /* malformed hrb and/or node-list + */ + JSVERIFY(FAIL, H5FD_s3comms_aws_canonical_request(cr_dest, 20, sh_dest, 20, NULL), + "http request object cannot be null") + + hrb = H5FD_s3comms_hrb_init_request("GET", "/", "HTTP/1.1"); + JSVERIFY(FAIL, H5FD_s3comms_aws_canonical_request(NULL, 20, sh_dest, 20, hrb), + "canonical request destination cannot be NULL") + + JSVERIFY(FAIL, H5FD_s3comms_aws_canonical_request(cr_dest, 20, NULL, 20, hrb), + "signed headers destination cannot be null") + + FAIL_IF(FAIL == H5FD_s3comms_hrb_destroy(&hrb)) + HDassert(NULL == hrb); + + PASSED(); + return 0; + +error: + + if (node != NULL) { + while (node != NULL) + (void)H5FD_s3comms_hrb_node_set(&node, node->name, NULL); + HDassert(node == NULL); + } + if (hrb != NULL) { + (void)H5FD_s3comms_hrb_destroy(&hrb); + } + + return -1; + +} /* end test_aws_canonical_request() */ + +/*--------------------------------------------------------------------------- + * + * Function: test_bytes_to_hex + * + * Purpose: + * + * Define and verify behavior of `H5FD_s3comms_bytes_to_hex()`. + * + * Return: + * + * Success: 0 + * Failure: -1 + * + * Programmer: Jacob Smith + * 2017-09-14 + * + *--------------------------------------------------------------------------- + */ +static herr_t +test_bytes_to_hex(void) +{ + /************************* + * test-local structures * + *************************/ + + struct testcase { + const char exp[17]; /* in size * 2 + 1 for null terminator */ + const unsigned char in[8]; + size_t size; + hbool_t lower; + }; + + /************************ + * test-local variables * + ************************/ + + struct testcase cases[] = { + { + "52F3000C9A", + {82, 243, 0, 12, 154}, + 5, + FALSE, + }, + { + "009a0cf3005200", /* lowercase alphas */ + {0, 154, 12, 243, 0, 82, 0}, + 7, + TRUE, + }, + { + "", {17, 63, 26, 56}, 0, FALSE, /* irrelevant */ + }, + }; + int i = 0; + int n_cases = 3; + char out[17]; + int out_off = 0; + + TESTING("bytes-to-hex"); + + for (i = 0; i < n_cases; i++) { + for (out_off = 0; out_off < 17; out_off++) { + out[out_off] = 0; + } + + JSVERIFY(SUCCEED, H5FD_s3comms_bytes_to_hex(out, cases[i].in, cases[i].size, cases[i].lower), NULL) + + JSVERIFY_STR(cases[i].exp, out, NULL) + } + + /* dest cannot be null + */ + JSVERIFY(FAIL, H5FD_s3comms_bytes_to_hex(NULL, (const unsigned char *)"nada", 5, FALSE), + "destination cannot be null") + + PASSED(); + return 0; + +error: + return -1; + +} /* end test_bytes_to_hex() */ + +/*--------------------------------------------------------------------------- + * + * Function: test_hrb_init_request() + * + * Purpose: + * + * Define and verify behavior of `H5FD_s3comms_hrb_init_request()` + * + * Programmer: Jacob Smith + * 2017-09-20 + * + *--------------------------------------------------------------------------- + */ +static herr_t +test_hrb_init_request(void) +{ + /********************* + * test-local macros * + *********************/ + + /************************* + * test-local structures * + *************************/ + + struct testcase { + const char msg[64]; + const char *verb; + const char *resource; + const char *exp_res; + const char *version; + hbool_t ret_null; + }; + + /************************ + * test-local variables * + ************************/ + + struct testcase cases[] = { + { + "get HTTP request just as we provided", + "GET", + "/path/to/some/file", + "/path/to/some/file", + "HTTP/1.1", + FALSE, + }, + { + "null verb substitues to GET", + NULL, + "/MYPATH/MYFILE.tiff", + "/MYPATH/MYFILE.tiff", + "HTTP/1.1", + FALSE, + }, + { + "demonstrate non-GET verb", + "HEAD", + "/MYPATH/MYFILE.tiff", + "/MYPATH/MYFILE.tiff", + "HTTP/1.1", + FALSE, + }, + { + "slash prepended to resource path, if necessary", + NULL, + "MYPATH/MYFILE.tiff", + "/MYPATH/MYFILE.tiff", + NULL, + FALSE, + }, + { + "null resource path causes problem", + "GET", + NULL, + NULL, + NULL, + TRUE, + }, + }; + struct testcase *C = NULL; + unsigned int i = 0; + unsigned int ncases = 5; + hrb_t * req = NULL; + + TESTING("hrb_init_request"); + + for (i = 0; i < ncases; i++) { + C = &cases[i]; + req = H5FD_s3comms_hrb_init_request(C->verb, C->resource, C->version); + if (cases[i].ret_null == TRUE) { + FAIL_IF(req != NULL); + } + else { + FAIL_IF(req == NULL); + JSVERIFY(S3COMMS_HRB_MAGIC, req->magic, NULL) + if (C->verb == NULL) { + JSVERIFY_STR("GET", req->verb, NULL) + } + else { + JSVERIFY_STR(req->verb, C->verb, NULL) + } + JSVERIFY_STR("HTTP/1.1", req->version, NULL) + JSVERIFY_STR(C->exp_res, req->resource, NULL) + FAIL_IF(req->first_header != NULL); + FAIL_IF(req->body != NULL); + JSVERIFY(0, req->body_len, NULL) + JSVERIFY(SUCCEED, H5FD_s3comms_hrb_destroy(&req), "unable to destroy hrb_t") + FAIL_IF(NULL != req); /* should annull pointer as well as free */ + } + + } /* end for each testcase */ + + PASSED(); + return 0; + +error: + (void)H5FD_s3comms_hrb_destroy(&req); + + return -1; + +} /* end test_hrb_init_request() */ + +/*--------------------------------------------------------------------------- + * + * Function: test_hrb_node_set() + * + * Purpose: + * + * Test operations on hrb_node_t structure + * + * Programmer: Jacob Smith + * 2017-09-22 + * + *--------------------------------------------------------------------------- + */ +static herr_t +test_hrb_node_set(void) +{ + /************************* + * test-local structures * + *************************/ + + /* bundle of name/value representing an hrb_node_t + */ + typedef struct node_mock_t { + const char *name; + const char *value; + } node_mock_t; + + /* bundle for a testcase + * + * `message` + * purpose of the testcase + * + * `delta` + * container for name and value strings to pass into node-set function + * to to modify the list. + * + * `returned` + * expected return value of node-set function + * + * `given` + * `expected` + * string arrays representing the state of the list before and after + * modification. The number of strings must be even, with each name + * paired to a value. `NULL` terminates the list, with `{NULL}` + * representing the empty list. + */ + typedef struct testcase { + const char *message; + node_mock_t delta; + herr_t returned; + const char *given[11]; /* name/value pairs in array; NULL sentinel */ + const char *expected[11]; + } testcase; + + /************************ + * test-local variables * + ************************/ + + testcase cases[] = { + { + "cannot remove node from null list", + {"Host", NULL}, + FAIL, + {NULL}, + {NULL}, + }, + { + "cannot create list with NULL field name", + {NULL, "somevalue"}, + FAIL, + {NULL}, + {NULL}, + }, + { + "create a new list", + {"Host", "somevalue"}, + SUCCEED, + {NULL}, + { + "Host", + "somevalue", + NULL, + }, + }, + { + "insert new node at head list", + {"Host", "somevalue"}, + SUCCEED, + { + "Range", + "bytes=20-40", + NULL, + }, + { + "Host", + "somevalue", + "Range", + "bytes=20-40", + NULL, + }, + }, + { + "append new node at list end", + {"x-amz-date", "somevalue"}, + SUCCEED, + { + "Range", + "bytes=20-40", + NULL, + }, + { + "Range", + "bytes=20-40", + "x-amz-date", + "somevalue", + NULL, + }, + }, + { + "insert new node inside list", + {"Intermediary", "somevalue"}, + SUCCEED, + { + "Host", + "somehost", + "Range", + "bytes=20-40", + NULL, + }, + { + "Host", + "somehost", + "Intermediary", + "somevalue", + "Range", + "bytes=20-40", + NULL, + }, + }, + { + "modify node", + {"Range", "bytes=40-80"}, + SUCCEED, + { + "Host", + "somehost", + "Range", + "bytes=20-40", + NULL, + }, + { + "Host", + "somehost", + "Range", + "bytes=40-80", + NULL, + }, + }, + { + "modify node with new case", + {"RANGE", "bytes=40-80"}, + SUCCEED, + { + "Host", + "somehost", + "Range", + "bytes=20-40", + NULL, + }, + { + "Host", + "somehost", + "RANGE", + "bytes=40-80", + NULL, + }, + }, + { + "cannot add node with no name", + {NULL, "bytes=40-80"}, + FAIL, + { + "Host", + "somehost", + NULL, + }, + { + "Host", + "somehost", + NULL, + }, + }, + { + "add node with 'empty' name", + {"", "bytes=40-80"}, + SUCCEED, + { + "Host", + "somehost", + NULL, + }, + { + "", + "bytes=40-80", + "Host", + "somehost", + NULL, + }, + }, + { + "remove node from end of list", + {"Host", NULL}, + SUCCEED, + { + "Date", + "Thr, 25 Jan 2018", + "Host", + "somehost", + NULL, + }, + { + "Date", + "Thr, 25 Jan 2018", + NULL, + }, + }, + { + "remove node from middle of list", + {"Host", NULL}, + SUCCEED, + { + "Date", + "Thr, 25 Jan 2018", + "Host", + "somehost", + "Range", + "bytes=20-40", + NULL, + }, + { + "Date", + "Thr, 25 Jan 2018", + "Range", + "bytes=20-40", + NULL, + }, + }, + { + "remove node from start of list", + {"Date", NULL}, + SUCCEED, + { + "Date", + "Thr, 25 Jan 2018", + "Host", + "somehost", + "Range", + "bytes=20-40", + NULL, + }, + { + "Host", + "somehost", + "Range", + "bytes=20-40", + NULL, + }, + }, + { + "remove only node in list", + {"Date", NULL}, + SUCCEED, + { + "Date", + "Thr, 25 Jan 2018", + NULL, + }, + { + NULL, + }, + }, + { + "attempt to remove absent node fails", + {"Host", NULL}, + FAIL, + { + "Date", + "Thr, 25 Jan 2018", + "Range", + "bytes=20-40", + NULL, + }, + { + "Date", + "Thr, 25 Jan 2018", + "Range", + "bytes=20-40", + NULL, + }, + }, + { + "removal is case-insensitive", + {"hOsT", NULL}, + SUCCEED, + { + "Date", + "Thr, 25 Jan 2018", + "Host", + "somehost", + "Range", + "bytes=20-40", + NULL, + }, + { + "Date", + "Thr, 25 Jan 2018", + "Range", + "bytes=20-40", + NULL, + }, + }, + }; + unsigned testcases_count = 16; + unsigned test_i = 0; + hrb_node_t *list = NULL; + + TESTING("hrb_node_t (test_hrb_node_set)"); + + for (test_i = 0; test_i < testcases_count; test_i++) { + const hrb_node_t *node = NULL; + const testcase * test = &(cases[test_i]); + unsigned mock_i = 0; + + /********* + * SETUP * + *********/ + + for (mock_i = 0; test->given[mock_i] != NULL; mock_i += 2) { + const char *name = test->given[mock_i]; + const char *valu = test->given[mock_i + 1]; + + FAIL_IF(SUCCEED != H5FD_s3comms_hrb_node_set(&list, name, valu)) + } + /******** + * TEST * + ********/ + + /* perform modification on list + */ + JSVERIFY(test->returned, H5FD_s3comms_hrb_node_set(&list, test->delta.name, test->delta.value), + test->message) + + /* verify resulting list + */ + node = list; + mock_i = 0; + while (test->expected[mock_i] != NULL && node != NULL) { + const char *name = test->expected[mock_i]; + const char *valu = test->expected[mock_i + 1]; + + JSVERIFY_STR(name, node->name, NULL) + JSVERIFY_STR(valu, node->value, NULL) + + mock_i += 2; + node = node->next; + } + FAIL_IF(test->expected[mock_i] != NULL) + FAIL_IF(node != NULL) + + /************ + * TEARDOWN * + ************/ + + while (list != NULL) { + FAIL_IF(SUCCEED != H5FD_s3comms_hrb_node_set(&list, list->name, NULL)) + } + } /* end for each testcase */ + + PASSED(); + return 0; + +error: + while (list != NULL) { + (void)H5FD_s3comms_hrb_node_set(&list, list->name, NULL); + } + + return -1; + +} /* end test_hrb_node_t() */ + +/*--------------------------------------------------------------------------- + * + * Function: test_HMAC_SHA256() + * + * Purpose: + * + * Define and verify behavior of `H5FD_s3comms_HMAC_SHA256()` + * + * Programmer: Jacob Smith + * 2017-09-19 + * + *--------------------------------------------------------------------------- + */ +static herr_t +test_HMAC_SHA256(void) +{ + + /************************* + * test-local structures * + *************************/ + + struct testcase { + herr_t ret; /* SUCCEED/FAIL expected from call */ + const unsigned char key[SHA256_DIGEST_LENGTH]; + size_t key_len; + const char * msg; + size_t msg_len; + const char * exp; /* not used if ret == FAIL */ + size_t dest_size; /* if 0, `dest` is not malloc'd */ + }; + + /************************ + * test-local variables * + ************************/ + + struct testcase cases[] = { + { + SUCCEED, + { + 0xdb, 0xb8, 0x93, 0xac, 0xc0, 0x10, 0x96, 0x49, 0x18, 0xf1, 0xfd, + 0x43, 0x3a, 0xdd, 0x87, 0xc7, 0x0e, 0x8b, 0x0d, 0xb6, 0xbe, 0x30, + 0xc1, 0xfb, 0xea, 0xfe, 0xfa, 0x5e, 0xc6, 0xba, 0x83, 0x78, + }, + SHA256_DIGEST_LENGTH, + "AWS4-HMAC-SHA256\n20130524T000000Z\n20130524/us-east-1/s3/" + "aws4_request\n7344ae5b7ee6c3e7e6b0fe0640412a37625d1fbfff95c48bbb2dc43964946972", + HDstrlen("AWS4-HMAC-SHA256\n20130524T000000Z\n20130524/us-east-1/s3/" + "aws4_request\n7344ae5b7ee6c3e7e6b0fe0640412a37625d1fbfff95c48bbb2dc43964946972"), + "f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41", + SHA256_DIGEST_LENGTH * 2 + 1, /* +1 for null terminator */ + }, + { + SUCCEED, + {'J', 'e', 'f', 'e'}, + 4, + "what do ya want for nothing?", + 28, + "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843", + SHA256_DIGEST_LENGTH * 2 + 1, + }, + { + FAIL, "DOESN'T MATTER", 14, "ALSO IRRELEVANT", 15, NULL, + 0, /* dest -> null, resulting in immediate error */ + }, + }; + char *dest = NULL; + int i = 0; + int n_cases = 3; + + TESTING("HMAC_SHA256"); + + for (i = 0; i < n_cases; i++) { + if (cases[i].dest_size == 0) { + dest = NULL; + } + else { + dest = (char *)HDmalloc(sizeof(char) * cases[i].dest_size); + HDassert(dest != NULL); + } + + JSVERIFY( + cases[i].ret, + H5FD_s3comms_HMAC_SHA256(cases[i].key, cases[i].key_len, cases[i].msg, cases[i].msg_len, dest), + cases[i].msg); + if (cases[i].ret == SUCCEED) { +#ifdef VERBOSE + if (0 != strncmp(cases[i].exp, dest, HDstrlen(cases[i].exp))) { + /* print out how wrong things are, and then fail + */ + dest = (char *)realloc(dest, cases[i].dest_size + 1); + HDassert(dest != NULL); + dest[cases[i].dest_size] = 0; + HDfprintf(stdout, "ERROR:\n!!! \"%s\"\n != \"%s\"\n", cases[i].exp, dest); + TEST_ERROR; + } +#else /* VERBOSE not defined */ + /* simple pass/fail test + */ + JSVERIFY(0, strncmp(cases[i].exp, dest, HDstrlen(cases[i].exp)), NULL); +#endif /* VERBOSE */ + } + free(dest); + } + + PASSED(); + return 0; + +error: + free(dest); + return -1; + +} /* end test_HMAC_SHA256() */ + +/*---------------------------------------------------------------------------- + * + * Function: test_nlowercase() + * + * Purpose: + * + * Define and verify behavior of `H5FD_s3comms_nlowercase()` + * + * Programmer: Jacob Smith + * 2017-19-18 + * + *---------------------------------------------------------------------------- + */ +static herr_t +test_nlowercase(void) +{ + /************************* + * test-local structures * + *************************/ + + struct testcase { + const char *in; + size_t len; + const char *exp; + }; + + /************************ + * test-local variables * + ************************/ + + /* any character after in exp on or after exp[len] is undefined. + * in this test, kept as the null character for simplicity. + */ + struct testcase cases[] = { + { + "HALlEluJAh", + 6, + "hallel", + }, + { + "all\0 lower", + 10, + "all\0 lower", + }, + { + "to meeeeeee", + 0, + "", + }, + }; + char *dest = NULL; + int i = 0; + int n_cases = 3; + + TESTING("nlowercase"); + + for (i = 0; i < n_cases; i++) { + dest = (char *)HDmalloc(sizeof(char) * 16); + + JSVERIFY(SUCCEED, H5FD_s3comms_nlowercase(dest, cases[i].in, cases[i].len), cases[i].in) + if (cases[i].len > 0) { + JSVERIFY(0, strncmp(dest, cases[i].exp, cases[i].len), NULL) + } + free(dest); + } /* end for each testcase */ + + JSVERIFY(FAIL, H5FD_s3comms_nlowercase(NULL, cases[0].in, cases[0].len), "null distination should fail") + + PASSED(); + return 0; + +error: + free(dest); + return -1; + +} /* end test_nlowercase() */ + +/*--------------------------------------------------------------------------- + * + * Function: test_parse_url() + * + * Programmer: Jacob Smith + * 2017-11-?? + * + *--------------------------------------------------------------------------- + */ +static herr_t +test_parse_url(void) +{ + /********************* + * test-local macros * + *********************/ + + /************************* + * test-local structures * + *************************/ + + typedef struct { + const char *scheme; + const char *host; + const char *port; + const char *path; + const char *query; + } const_purl_t; + + struct testcase { + const char *url; + herr_t exp_ret; /* expected return; */ + /* if FAIL, `expected` is unused */ + const_purl_t expected; + const char * msg; + }; + + /************************ + * test-local variables * + ************************/ + + parsed_url_t * purl = NULL; + unsigned int i = 0; + unsigned int ncases = 15; + struct testcase cases[] = { + { + NULL, + FAIL, + {NULL, NULL, NULL, NULL, NULL}, + "null url", + }, + { + "", + FAIL, + {NULL, NULL, NULL, NULL, NULL}, + "empty url", + }, + { + "ftp://[1000:4000:0002:2010]", + SUCCEED, + { + "ftp", + "[1000:4000:0002:2010]", + NULL, + NULL, + NULL, + }, + "IPv6 ftp and empty path (root)", + }, + { + "ftp://[1000:4000:0002:2010]:2040", + SUCCEED, + { + "ftp", + "[1000:4000:0002:2010]", + "2040", + NULL, + NULL, + }, + "root IPv6 ftp with port", + }, + { + "http://some.domain.org:9000/path/to/resource.txt", + SUCCEED, + { + "http", + "some.domain.org", + "9000", + "path/to/resource.txt", + NULL, + }, + "without query", + }, + { + "https://domain.me:00/file.txt?some_params unchecked", + SUCCEED, + { + "https", + "domain.me", + "00", + "file.txt", + "some_params unchecked", + }, + "with query", + }, + { + "ftp://domain.com/", + SUCCEED, + { + "ftp", + "domain.com", + NULL, + NULL, + NULL, + }, + "explicit root w/out port", + }, + { + "ftp://domain.com:1234/", + SUCCEED, + { + "ftp", + "domain.com", + "1234", + NULL, + NULL, + }, + "explicit root with port", + }, + { + "ftp://domain.com:1234/file?", + FAIL, + { + NULL, + NULL, + NULL, + NULL, + NULL, + }, + "empty query is invalid", + }, + { + "ftp://:1234/file", + FAIL, + { + NULL, + NULL, + NULL, + NULL, + NULL, + }, + "no host", + }, + { + "h&r block", + FAIL, + { + NULL, + NULL, + NULL, + NULL, + NULL, + }, + "no scheme (bad URL)", + }, + { + "http://domain.com?a=b&d=b", + SUCCEED, + { + "http", + "domain.com", + NULL, + NULL, + "a=b&d=b", + }, + "QUERY with implict PATH", + }, + { + "http://[5]/path?a=b&d=b", + SUCCEED, + { + "http", + "[5]", + NULL, + "path", + "a=b&d=b", + }, + "IPv6 extraction is really dumb", + }, + { + "http://[1234:5678:0910:1112]:port/path", + FAIL, + { + NULL, + NULL, + NULL, + NULL, + NULL, + }, + "non-decimal PORT (port)", + }, + { + "http://mydomain.com:01a3/path", + FAIL, + { + NULL, + NULL, + NULL, + NULL, + NULL, + }, + "non-decimal PORT (01a3)", + }, + }; + + TESTING("url-parsing functionality"); + + /********* + * TESTS * + *********/ + + for (i = 0; i < ncases; i++) { + HDassert(purl == NULL); + + JSVERIFY(cases[i].exp_ret, H5FD_s3comms_parse_url(cases[i].url, &purl), cases[i].msg) + + if (cases[i].exp_ret == FAIL) { + /* on FAIL, `purl` should be untouched--remains NULL */ + FAIL_UNLESS(purl == NULL) + } + else { + /* on SUCCEED, `purl` should be set */ + FAIL_IF(purl == NULL) + + if (cases[i].expected.scheme != NULL) { + FAIL_IF(NULL == purl->scheme) + JSVERIFY_STR(cases[i].expected.scheme, purl->scheme, cases[i].msg) + } + else { + FAIL_UNLESS(NULL == purl->scheme) + } + + if (cases[i].expected.host != NULL) { + FAIL_IF(NULL == purl->host) + JSVERIFY_STR(cases[i].expected.host, purl->host, cases[i].msg) + } + else { + FAIL_UNLESS(NULL == purl->host) + } + + if (cases[i].expected.port != NULL) { + FAIL_IF(NULL == purl->port) + JSVERIFY_STR(cases[i].expected.port, purl->port, cases[i].msg) + } + else { + FAIL_UNLESS(NULL == purl->port) + } + + if (cases[i].expected.path != NULL) { + FAIL_IF(NULL == purl->path) + JSVERIFY_STR(cases[i].expected.path, purl->path, cases[i].msg) + } + else { + FAIL_UNLESS(NULL == purl->path) + } + + if (cases[i].expected.query != NULL) { + FAIL_IF(NULL == purl->query) + JSVERIFY_STR(cases[i].expected.query, purl->query, cases[i].msg) + } + else { + FAIL_UNLESS(NULL == purl->query) + } + } /* end if parse-url return SUCCEED/FAIL */ + + /* per-test cleanup + * well-behaved, even if `purl` is NULL + */ + FAIL_IF(FAIL == H5FD_s3comms_free_purl(purl)) + purl = NULL; + + } /* end for each testcase */ + + PASSED(); + return 0; + +error: + /*********** + * cleanup * + ***********/ + (void)H5FD_s3comms_free_purl(purl); + + return -1; + +} /* end test_parse_url() */ + +/*--------------------------------------------------------------------------- + * + * Function: test_percent_encode_char() + * + * Purpose: + * + * Define and verify behavior of `H5FD_s3comms_percent_encode_char()` + * + * Return: + * + * Success: 0 + * Failure: -1 + * + * Programmer: Jacob Smith + * 2017-09-14 + * + *--------------------------------------------------------------------------- + */ +static herr_t +test_percent_encode_char(void) +{ + /************************* + * test-local structures * + *************************/ + + struct testcase { + const char c; + const char *exp; + size_t exp_len; + }; + + /************************ + * test-local variables * + ************************/ + + struct testcase cases[] = { + {'$', "%24", 3}, /* u+0024 dollar sign */ + {' ', "%20", 3}, /* u+0020 space */ + {'^', "%5E", 3}, /* u+0094 carat */ + {'/', "%2F", 3}, /* u+002f solidus (forward slash) */ + /* {??, "%C5%8C", 6},*/ /* u+014c Latin Capital Letter O with Macron */ + /* Not included because it is multibyte "wide" character that poses */ + /* issues both in the underlying function and in being written in */ + /* this file. */ + /* {'¢', "%C2%A2", 6}, */ /* u+00a2 cent sign */ + /* above works, but complains about wide character overflow */ + /* Elide for now, until it is determined (a) unnecessary or */ + /* (b) requiring signature change to accommodate wide characters */ + {'\0', "%00", 3}, /* u+0000 null */ + }; + char dest[13]; + size_t dest_len = 0; + int i = 0; + int n_cases = 5; + + TESTING("percent encode characters"); + + for (i = 0; i < n_cases; i++) { + JSVERIFY(SUCCEED, H5FD_s3comms_percent_encode_char(dest, (const unsigned char)cases[i].c, &dest_len), + NULL) + JSVERIFY(cases[i].exp_len, dest_len, NULL) + JSVERIFY(0, strncmp(dest, cases[i].exp, dest_len), NULL) + JSVERIFY_STR(cases[i].exp, dest, NULL) + } + + JSVERIFY(FAIL, H5FD_s3comms_percent_encode_char(NULL, (const unsigned char)'^', &dest_len), NULL) + + PASSED(); + return 0; + +error: + return -1; +} /* end test_percent_encode_char() */ + +/*--------------------------------------------------------------------------- + * Function: test_s3r_open() + * + * Programmer: Jacob Smith 2018-01-24 + * + * Changes: None + * + *--------------------------------------------------------------------------- + */ +static herr_t +test_s3r_get_filesize(void) +{ + + /************************ + * test-local variables * + ************************/ + + char url_raven[S3_TEST_MAX_URL_SIZE]; + s3r_t *handle = NULL; + + TESTING("s3r_get_filesize"); + + /* setup -- compose url to target resource + */ + if (FALSE == s3_test_bucket_defined) { + SKIPPED(); + puts(" environment variable HDF5_ROS3_TEST_BUCKET_URL not defined"); + fflush(stdout); + return 0; + } + + FAIL_IF(S3_TEST_MAX_URL_SIZE < HDsnprintf(url_raven, S3_TEST_MAX_URL_SIZE, "%s/%s", s3_test_bucket_url, + S3_TEST_RESOURCE_TEXT_PUBLIC)); + + JSVERIFY(0, H5FD_s3comms_s3r_get_filesize(NULL), "filesize of the null handle should be 0") + + handle = H5FD_s3comms_s3r_open(url_raven, NULL, NULL, NULL); + FAIL_IF(handle == NULL) + + JSVERIFY(6464, H5FD_s3comms_s3r_get_filesize(handle), NULL) + + FAIL_IF(SUCCEED != H5FD_s3comms_s3r_close(handle)) + + PASSED(); + return 0; + +error: + if (handle != NULL) + (void)H5FD_s3comms_s3r_close(handle); + + return -1; + +} /* end test_s3r_get_filesize() */ + +/*--------------------------------------------------------------------------- + * Function: test_s3r_open() + * + * Programmer: Jacob Smith 2018-01-?? + * + * Changes: None + * + *--------------------------------------------------------------------------- + */ +static herr_t +test_s3r_open(void) +{ + + /************************ + * test-local variables * + ************************/ + + char url_missing[S3_TEST_MAX_URL_SIZE]; + char url_raven[S3_TEST_MAX_URL_SIZE]; + char url_raven_badport[S3_TEST_MAX_URL_SIZE]; + char url_shakespeare[S3_TEST_MAX_URL_SIZE]; + unsigned char signing_key[SHA256_DIGEST_LENGTH]; + struct tm * now = NULL; + char iso8601now[ISO8601_SIZE]; + s3r_t * handle = NULL; + hbool_t curl_ready = FALSE; + parsed_url_t *purl = NULL; + + TESTING("s3r_open"); + + if (s3_test_credentials_loaded == 0) { + SKIPPED(); + puts(" s3 credentials are not loaded"); + fflush(stdout); + return 0; + } + if (FALSE == s3_test_bucket_defined) { + SKIPPED(); + puts(" environment variable HDF5_ROS3_TEST_BUCKET_URL not defined"); + fflush(stdout); + return 0; + } + + /****************** + * PRE-TEST SETUP * + ******************/ + + FAIL_IF(S3_TEST_MAX_URL_SIZE < HDsnprintf(url_shakespeare, S3_TEST_MAX_URL_SIZE, "%s/%s", + s3_test_bucket_url, S3_TEST_RESOURCE_TEXT_RESTRICTED)); + + FAIL_IF(S3_TEST_MAX_URL_SIZE < HDsnprintf(url_missing, S3_TEST_MAX_URL_SIZE, "%s/%s", s3_test_bucket_url, + S3_TEST_RESOURCE_MISSING)); + + FAIL_IF(S3_TEST_MAX_URL_SIZE < HDsnprintf(url_raven, S3_TEST_MAX_URL_SIZE, "%s/%s", s3_test_bucket_url, + S3_TEST_RESOURCE_TEXT_PUBLIC)); + + /* Set given bucket url with invalid/inactive port number for badport. + * Note, this sort of micro-management of parsed_url_t is not advised + */ + FAIL_IF(FAIL == H5FD_s3comms_parse_url(s3_test_bucket_url, &purl)) + if (purl->port == NULL) { + purl->port = (char *)H5MM_malloc(sizeof(char) * 5); + FAIL_IF(purl->port == NULL); + FAIL_IF(5 < HDsnprintf(purl->port, 5, "9000")) + } + else if (strcmp(purl->port, "9000") != 0) { + FAIL_IF(5 < HDsnprintf(purl->port, 5, "9000")) + } + else { + FAIL_IF(5 < HDsnprintf(purl->port, 5, "1234")) + } + FAIL_IF(S3_TEST_MAX_URL_SIZE < HDsnprintf(url_raven_badport, S3_TEST_MAX_URL_SIZE, "%s://%s:%s/%s", + purl->scheme, purl->host, purl->port, + S3_TEST_RESOURCE_TEXT_PUBLIC)); + + curl_global_init(CURL_GLOBAL_DEFAULT); + curl_ready = TRUE; + + now = gmnow(); + FAIL_IF(now == NULL) + FAIL_IF(ISO8601NOW(iso8601now, now) != (ISO8601_SIZE - 1)); + + /* It is desired to have means available to verify that signing_key + * was set successfully and to an expected value. + */ + FAIL_IF(FAIL == H5FD_s3comms_signing_key(signing_key, (const char *)s3_test_aws_secret_access_key, + (const char *)s3_test_aws_region, (const char *)iso8601now)); + + /************************* + * OPEN NONEXISTENT FILE * + *************************/ + + /* attempt anonymously + */ + handle = H5FD_s3comms_s3r_open(url_missing, NULL, NULL, NULL); + FAIL_IF(handle != NULL); + + /* attempt with authentication + */ + handle = + H5FD_s3comms_s3r_open(url_missing, (const char *)s3_test_aws_region, + (const char *)s3_test_aws_access_key_id, (const unsigned char *)signing_key); + FAIL_IF(handle != NULL); + + /************************* + * INACTIVE PORT ON HOST * + *************************/ + +#if S3_TEST_RUN_TIMEOUT + HDprintf("Opening on inactive port may hang for a minute; waiting for timeout\n"); + handle = H5FD_s3comms_s3r_open(url_raven_badport, NULL, NULL, NULL); + FAIL_IF(handle != NULL); +#endif + + /******************************* + * INVALID AUTHENTICATION INFO * + *******************************/ + + /* anonymous access on restricted file + */ + handle = H5FD_s3comms_s3r_open(url_shakespeare, NULL, NULL, NULL); + FAIL_IF(handle != NULL); + + /* passed in a bad ID + */ + handle = H5FD_s3comms_s3r_open(url_shakespeare, (const char *)s3_test_aws_region, "I_MADE_UP_MY_ID", + (const unsigned char *)signing_key); + FAIL_IF(handle != NULL); + + /* using an invalid signing key + */ + handle = + H5FD_s3comms_s3r_open(url_shakespeare, (const char *)s3_test_aws_region, + (const char *)s3_test_aws_access_key_id, (const unsigned char *)EMPTY_SHA256); + FAIL_IF(handle != NULL); + + /******************************* + * SUCCESSFUL OPEN (AND CLOSE) * + *******************************/ + + /* anonymous + */ + handle = H5FD_s3comms_s3r_open(url_raven, NULL, NULL, NULL); + FAIL_IF(handle == NULL); + JSVERIFY(6464, H5FD_s3comms_s3r_get_filesize(handle), "did not get expected filesize") + JSVERIFY(SUCCEED, H5FD_s3comms_s3r_close(handle), "unable to close file") + handle = NULL; + + /* using authentication on anonymously-accessible file? + */ + handle = + H5FD_s3comms_s3r_open(url_raven, (const char *)s3_test_aws_region, + (const char *)s3_test_aws_access_key_id, (const unsigned char *)signing_key); + FAIL_IF(handle == NULL); + JSVERIFY(6464, H5FD_s3comms_s3r_get_filesize(handle), NULL) + JSVERIFY(SUCCEED, H5FD_s3comms_s3r_close(handle), "unable to close file") + handle = NULL; + + /* authenticating + */ + handle = + H5FD_s3comms_s3r_open(url_shakespeare, (const char *)s3_test_aws_region, + (const char *)s3_test_aws_access_key_id, (const unsigned char *)signing_key); + FAIL_IF(handle == NULL); + JSVERIFY(5458199, H5FD_s3comms_s3r_get_filesize(handle), NULL) + JSVERIFY(SUCCEED, H5FD_s3comms_s3r_close(handle), "unable to close file") + handle = NULL; + + curl_global_cleanup(); + curl_ready = FALSE; + + FAIL_IF(FAIL == H5FD_s3comms_free_purl(purl)) + purl = NULL; + + PASSED(); + return 0; +error: + /*********** + * cleanup * + ***********/ + + if (handle != NULL) + H5FD_s3comms_s3r_close(handle); + if (purl != NULL) + H5FD_s3comms_free_purl(purl); + if (curl_ready == TRUE) + curl_global_cleanup(); + + return -1; + +} /* end test_s3r_open() */ + +/*--------------------------------------------------------------------------- + * + * Function: test_s3r_read() + * + * Purpose: + * + * Specify and demonstrate the use and life cycle of an S3 Request handle + * `s3r_t`, through its related functions. + * + * H5FD_s3comms_s3r_open + * H5FD_s3comms_s3r_getsize << called by open() _only_ + * H5FD_s3comms_s3r_read << called by getsize(), multiple times working + * H5FD_s3comms_s3r_close + * + * Shows most basic curl interation. + * + * Programmer: Jacob Smith + * 2017-10-06 + * + *--------------------------------------------------------------------------- + */ +static herr_t +test_s3r_read(void) +{ + +#define S3COMMS_TEST_BUFFER_SIZE 256 + + /************************ + * test-local variables * + ************************/ + + char url_raven[S3_TEST_MAX_URL_SIZE]; + char buffer[S3COMMS_TEST_BUFFER_SIZE]; + s3r_t * handle = NULL; + hbool_t curl_ready = FALSE; + unsigned int i = 0; + + TESTING("test_s3r_read"); + + /* + * initial setup + */ + if (FALSE == s3_test_bucket_defined) { + SKIPPED(); + puts(" environment variable HDF5_ROS3_TEST_BUCKET_URL not defined"); + fflush(stdout); + return 0; + } + + curl_global_init(CURL_GLOBAL_DEFAULT); + curl_ready = TRUE; + FAIL_IF(S3_TEST_MAX_URL_SIZE < HDsnprintf(url_raven, S3_TEST_MAX_URL_SIZE, "%s/%s", s3_test_bucket_url, + S3_TEST_RESOURCE_TEXT_PUBLIC)); + + for (i = 0; i < S3COMMS_TEST_BUFFER_SIZE; i++) + buffer[i] = '\0'; + + /* open file + */ + handle = H5FD_s3comms_s3r_open(url_raven, NULL, NULL, NULL); + FAIL_IF(handle == NULL) + JSVERIFY(6464, H5FD_s3comms_s3r_get_filesize(handle), NULL) + + for (i = 0; i < S3COMMS_TEST_BUFFER_SIZE; i++) + buffer[i] = '\0'; + + /********************** + * read start of file * + **********************/ + + JSVERIFY(SUCCEED, H5FD_s3comms_s3r_read(handle, (haddr_t)0, (size_t)118, buffer), NULL) + JSVERIFY_STR("Once upon a midnight dreary, while I pondered, weak and weary,\n" + "Over many a quaint and curious volume of forgotten lore", + buffer, NULL) + + for (i = 0; i < S3COMMS_TEST_BUFFER_SIZE; i++) + buffer[i] = '\0'; + + /************************ + * read arbitrary range * + ************************/ + + JSVERIFY(SUCCEED, H5FD_s3comms_s3r_read(handle, (haddr_t)2540, (size_t)54, buffer), NULL) + JSVERIFY_STR("the grave and stern decorum of the countenance it wore", buffer, NULL) + + for (i = 0; i < S3COMMS_TEST_BUFFER_SIZE; i++) + buffer[i] = '\0'; + + /********************** + * read one character * + **********************/ + + JSVERIFY(SUCCEED, H5FD_s3comms_s3r_read(handle, (haddr_t)2540, (size_t)1, buffer), NULL) + JSVERIFY_STR("t", buffer, NULL) + + for (i = 0; i < S3COMMS_TEST_BUFFER_SIZE; i++) + buffer[i] = '\0'; + + /*************** + * read to EoF * + ***************/ + + JSVERIFY(SUCCEED, H5FD_s3comms_s3r_read(handle, (haddr_t)6370, (size_t)0, buffer), NULL) + JSVERIFY( + 0, + strncmp( + buffer, + "And my soul from out that shadow that lies floating on the floor\nShall be lifted—nevermore!\n", + 94), + buffer) + + for (i = 0; i < S3COMMS_TEST_BUFFER_SIZE; i++) + buffer[i] = '\0'; + + /***************** + * read past eof * + *****************/ + + JSVERIFY(FAIL, + H5FD_s3comms_s3r_read(handle, (haddr_t)6400, (size_t)100, /* 6400+100 > 6464 */ + buffer), + NULL) + JSVERIFY(0, strcmp("", buffer), NULL) + + /************************ + * read starts past eof * + ************************/ + + JSVERIFY(FAIL, + H5FD_s3comms_s3r_read(handle, (haddr_t)1200699, /* 1200699 > 6464 */ + (size_t)100, buffer), + NULL) + JSVERIFY(0, strcmp("", buffer), NULL) + + /********************** + * read starts on eof * + **********************/ + + JSVERIFY(FAIL, H5FD_s3comms_s3r_read(handle, (haddr_t)6464, (size_t)0, buffer), NULL) + JSVERIFY(0, strcmp("", buffer), NULL) + + /************* + * TEAR DOWN * + *************/ + + JSVERIFY(SUCCEED, H5FD_s3comms_s3r_close(handle), "unable to close file") + handle = NULL; + + curl_global_cleanup(); + curl_ready = FALSE; + + PASSED(); + return 0; + +error: + /*********** + * cleanup * + ***********/ + + if (handle != NULL) + H5FD_s3comms_s3r_close(handle); + + if (curl_ready == TRUE) + curl_global_cleanup(); + + return -1; + +#undef S3COMMS_TEST_BUFFER_SIZE + +} /* end test_s3r_read() */ + +/*--------------------------------------------------------------------------- + * + * Function: test_signing_key() + * + * Purpose: + * + * Define and verify behavior of `H5FD_s3comms_signing_key()` + * + * More test cases would be a very good idea. + * + * Programmer: Jacob Smith + * 2017-09-18 + * + *--------------------------------------------------------------------------- + */ +static herr_t +test_signing_key(void) +{ + /************************* + * test-local structures * + *************************/ + + struct testcase { + const char * region; + const char * secret_key; + const char * when; + unsigned char exp[SHA256_DIGEST_LENGTH]; + }; + + /************************ + * test-local variables * + ************************/ + + struct testcase cases[] = { + { + "us-east-1", + "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + "20130524T000000Z", + { + 0xdb, 0xb8, 0x93, 0xac, 0xc0, 0x10, 0x96, 0x49, 0x18, 0xf1, 0xfd, + 0x43, 0x3a, 0xdd, 0x87, 0xc7, 0x0e, 0x8b, 0x0d, 0xb6, 0xbe, 0x30, + 0xc1, 0xfb, 0xea, 0xfe, 0xfa, 0x5e, 0xc6, 0xba, 0x83, 0x78, + }, + }, + }; + int i = 0; + unsigned char *key = NULL; + int ncases = 1; + + TESTING("signing_key"); + + for (i = 0; i < ncases; i++) { + key = (unsigned char *)HDmalloc(sizeof(unsigned char) * SHA256_DIGEST_LENGTH); + HDassert(key != NULL); + + JSVERIFY(SUCCEED, H5FD_s3comms_signing_key(key, cases[i].secret_key, cases[i].region, cases[i].when), + NULL) + + JSVERIFY(0, strncmp((const char *)cases[i].exp, (const char *)key, SHA256_DIGEST_LENGTH), + cases[i].exp) + + free(key); + key = NULL; + } + + /*************** + * ERROR CASES * + ***************/ + + key = (unsigned char *)HDmalloc(sizeof(unsigned char) * SHA256_DIGEST_LENGTH); + HDassert(key != NULL); + + JSVERIFY(FAIL, H5FD_s3comms_signing_key(NULL, cases[0].secret_key, cases[0].region, cases[0].when), + "destination cannot be NULL") + + JSVERIFY(FAIL, H5FD_s3comms_signing_key(key, NULL, cases[0].region, cases[0].when), + "secret key cannot be NULL") + + JSVERIFY(FAIL, H5FD_s3comms_signing_key(key, cases[0].secret_key, NULL, cases[0].when), + "aws region cannot be NULL") + + JSVERIFY(FAIL, H5FD_s3comms_signing_key(key, cases[0].secret_key, cases[0].region, NULL), + "time string cannot be NULL") + + free(key); + key = NULL; + + PASSED(); + return 0; + +error: + if (key != NULL) { + free(key); + } + + return -1; + +} /* end test_signing_key() */ + +/*--------------------------------------------------------------------------- + * + * Function: test_tostringtosign() + * + * Purpose: + * + * Verify that we can get the "string to sign" from a Canonical Request and + * related information. + * + * Demonstrate failure cases. + * + * Return: + * + * Success: 0 + * Failure: -1 + * + * Programmer: Jacob Smith + * 2017-09-13 + * + *--------------------------------------------------------------------------- + */ +static herr_t +test_tostringtosign(void) +{ + /************************ + * test-local variables * + ************************/ + + const char canonreq[] = "GET\n/" + "test.txt\n\nhost:examplebucket.s3.amazonaws.com\nrange:bytes=0-9\nx-amz-content-" + "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-" + "date:20130524T000000Z\n\nhost;range;x-amz-content-sha256;x-amz-" + "date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + const char iso8601now[] = "20130524T000000Z"; + const char region[] = "us-east-1"; + char s2s[512]; + + TESTING("s3comms tostringtosign"); + + JSVERIFY(SUCCEED, H5FD_s3comms_tostringtosign(s2s, canonreq, iso8601now, region), + "unable to create string to sign") + + JSVERIFY_STR("AWS4-HMAC-SHA256\n20130524T000000Z\n20130524/us-east-1/s3/" + "aws4_request\n7344ae5b7ee6c3e7e6b0fe0640412a37625d1fbfff95c48bbb2dc43964946972", + s2s, NULL) + + JSVERIFY(FAIL, H5FD_s3comms_tostringtosign(s2s, NULL, iso8601now, region), + "canonical request string cannot be NULL") + + JSVERIFY(FAIL, H5FD_s3comms_tostringtosign(s2s, canonreq, NULL, region), "time string cannot be NULL") + + JSVERIFY(FAIL, H5FD_s3comms_tostringtosign(s2s, canonreq, iso8601now, NULL), "aws region cannot be NULL") + + PASSED(); + return 0; + +error: + return -1; + +} /* end test_tostringtosign() */ + +/*---------------------------------------------------------------------------- + * + * Function: test_trim() + * + * Purpose: + * + * Define and verify behavior of `H5FD_s3comms_trim()`. + * + * Programmer: Jacob Smith + * 2017-09-14 + * + *---------------------------------------------------------------------------- + */ +static herr_t +test_trim(void) +{ + /************************* + * test-local structures * + *************************/ + + struct testcase { + const char *in; + size_t in_len; + const char *exp; + size_t exp_len; + }; + + /************************ + * test-local variables * + ************************/ + + struct testcase cases[] = { + { + "block string", + 12, + "block string", + 12, + }, + { + " \n\r \t", + 6, + "", + 0, + }, + { + " \twhite b4", + 10, + "white b4", + 8, + }, + { + "white after\r\n ", + 15, + "white after", + 11, + }, + { + " on\nends\t", + 9, + "on\nends", + 7, + }, + }; + char dest[32]; + size_t dest_len = 0; + int i = 0; + int n_cases = 5; + char * str = NULL; + + TESTING("s3comms trim"); + + for (i = 0; i < n_cases; i++) { + HDassert(str == NULL); + str = (char *)HDmalloc(sizeof(char) * cases[i].in_len); + HDassert(str != NULL); + HDstrncpy(str, cases[i].in, cases[i].in_len); + + JSVERIFY(SUCCEED, H5FD_s3comms_trim(dest, str, cases[i].in_len, &dest_len), NULL) + JSVERIFY(cases[i].exp_len, dest_len, cases[i].in) + if (dest_len > 0) { + JSVERIFY(0, strncmp(cases[i].exp, dest, dest_len), cases[i].exp) + } + free(str); + str = NULL; + } /* end for each testcase */ + + JSVERIFY(SUCCEED, H5FD_s3comms_trim(dest, NULL, 3, &dest_len), + "should not fail when trimming a null string"); + JSVERIFY(0, dest_len, "trimming NULL string writes 0 characters") + + HDassert(str == NULL); + str = (char *)HDmalloc(sizeof(char *) * 11); + HDassert(str != NULL); + memcpy(str, "some text ", 11); /* string with null terminator */ + JSVERIFY(FAIL, H5FD_s3comms_trim(NULL, str, 10, &dest_len), "destination for trim cannot be NULL"); + free(str); + str = NULL; + + PASSED(); + return 0; + +error: + if (str != NULL) { + free(str); + } + return -1; + +} /* end test_trim() */ + +/*---------------------------------------------------------------------------- + * + * Function: test_uriencode() + * + * Purpose: + * + * Define and verify behavior of `H5FD_s3comms_uriencode()`. + * + * Programmer: Jacob Smith + * 2017-09-14 + * + *---------------------------------------------------------------------------- + */ +static herr_t +test_uriencode(void) +{ + /************************* + * test-local structures * + *************************/ + + struct testcase { + const char *str; + size_t s_len; + hbool_t encode_slash; + const char *expected; + }; + + /************************ + * test-local variables * + ************************/ + + struct testcase cases[] = {{ + "/path/to/resource.jpg", + 21, + FALSE, + "/path/to/resource.jpg", + }, + { + "/path/to/resource.jpg", + 21, + TRUE, + "%2Fpath%2Fto%2Fresource.jpg", + }, + { + "string got_spaa ces", + 20, + TRUE, + "string%20got_spaa%20%20ces", + }, + { + "sp ac~es/and-sl ash.encoded", + 27, + TRUE, + "sp%20ac~es%2Fand-sl%20ash.encoded", + }, + { + "sp ac~es/and-sl ash.unencoded", + 29, + FALSE, + "sp%20ac~es/and-sl%20ash.unencoded", + }, + { + "/path/to/resource.txt", + 0, + FALSE, + "", + + }}; + char * dest = NULL; + size_t dest_written = 0; + int i = 0; + int ncases = 6; + size_t str_len = 0; + + TESTING("s3comms uriencode") + + for (i = 0; i < ncases; i++) { + str_len = cases[i].s_len; + dest = (char *)HDmalloc(sizeof(char) * str_len * 3 + 1); + FAIL_IF(dest == NULL) + + JSVERIFY(SUCCEED, + H5FD_s3comms_uriencode(dest, cases[i].str, str_len, cases[i].encode_slash, &dest_written), + NULL); + JSVERIFY(HDstrlen(cases[i].expected), dest_written, NULL) + JSVERIFY(0, strncmp(dest, cases[i].expected, dest_written), cases[i].expected); + + free(dest); + dest = NULL; + } /* end for each testcase */ + + /*************** + * ERROR CASES * + ***************/ + + dest = (char *)HDmalloc(sizeof(char) * 15); + HDassert(dest != NULL); + + JSVERIFY(FAIL, H5FD_s3comms_uriencode(NULL, "word$", 5, FALSE, &dest_written), + "destination cannot be NULL"); + JSVERIFY(FAIL, H5FD_s3comms_uriencode(dest, NULL, 5, FALSE, &dest_written), + "source string cannot be NULL"); + + free(dest); + dest = NULL; + + PASSED(); + return 0; + +error: + if (dest != NULL) { + free(dest); + } + return -1; + +} /* end test_uriencode() */ + +#endif /* H5_HAVE_ROS3_VFD */ + +/*------------------------------------------------------------------------- + * Function: main() + * + * Purpose: + * + * Run unit tests for S3 Communications (s3comms). + * + * Return: + * + * Success: 0 + * Failure: 1 + * + * Programmer: Jacob Smith + * 2017-10-12 + * + *------------------------------------------------------------------------- + */ +int +main(void) +{ +#ifdef H5_HAVE_ROS3_VFD + int nerrors = 0; + const char *bucket_url_env = NULL; + + h5_reset(); + +#endif /* H5_HAVE_ROS3_VFD */ + + HDprintf("Testing S3Communications functionality.\n"); + +#ifdef H5_HAVE_ROS3_VFD + + /* "clear" profile data strings */ + s3_test_aws_access_key_id[0] = '\0'; + s3_test_aws_secret_access_key[0] = '\0'; + s3_test_aws_region[0] = '\0'; + s3_test_bucket_url[0] = '\0'; + + /* TODO: unit/regression test for H5FD_s3comms_load_aws_profile() + * requires a few test files and/or manipulation of default path + */ + /* attempt to load test credentials + * if unable, certain tests will be skipped + */ + if (SUCCEED == H5FD_s3comms_load_aws_profile(S3_TEST_PROFILE_NAME, s3_test_aws_access_key_id, + s3_test_aws_secret_access_key, s3_test_aws_region)) { + s3_test_credentials_loaded = 1; + } + + bucket_url_env = HDgetenv("HDF5_ROS3_TEST_BUCKET_URL"); + if (bucket_url_env == NULL || bucket_url_env[0] == '\0') { + HDprintf("WARNING: S3 bucket url is not defined in enviornment " + "variable 'HDF5_ROS3_TEST_BUCKET_URL'!\n"); + } + else { + HDstrncpy(s3_test_bucket_url, bucket_url_env, S3_TEST_MAX_URL_SIZE); + s3_test_bucket_defined = TRUE; + } + + /* tests ordered rougly by dependence */ + nerrors += test_macro_format_credential() < 0 ? 1 : 0; + nerrors += test_trim() < 0 ? 1 : 0; + nerrors += test_nlowercase() < 0 ? 1 : 0; + nerrors += test_uriencode() < 0 ? 1 : 0; + nerrors += test_percent_encode_char() < 0 ? 1 : 0; + nerrors += test_bytes_to_hex() < 0 ? 1 : 0; + nerrors += test_HMAC_SHA256() < 0 ? 1 : 0; + nerrors += test_signing_key() < 0 ? 1 : 0; + nerrors += test_hrb_node_set() < 0 ? 1 : 0; + nerrors += test_hrb_init_request() < 0 ? 1 : 0; + nerrors += test_parse_url() < 0 ? 1 : 0; + nerrors += test_aws_canonical_request() < 0 ? 1 : 0; + nerrors += test_tostringtosign() < 0 ? 1 : 0; + nerrors += test_s3r_open() < 0 ? 1 : 0; + nerrors += test_s3r_get_filesize() < 0 ? 1 : 0; + nerrors += test_s3r_read() < 0 ? 1 : 0; + + if (nerrors) { + HDprintf("***** %d S3comms TEST%s FAILED! *****\n", nerrors, nerrors > 1 ? "S" : ""); + return 1; + } + + HDprintf("All S3comms tests passed.\n"); + + return 0; + +#else + + HDprintf("SKIPPED - read-only S3 VFD not built\n"); + return EXIT_SUCCESS; + +#endif /* H5_HAVE_ROS3_VFD */ + +} /* end main() */ |