diff --git a/.gitignore b/.gitignore index c6ef4144205..b86d970d02b 100644 --- a/.gitignore +++ b/.gitignore @@ -122,6 +122,7 @@ plugins/esi/vars_test plugins/experimental/slice/test_config plugins/experimental/slice/test_content_range plugins/experimental/slice/test_range +plugins/experimental/uri_signing/test_uri_signing mgmt/api/traffic_api_cli_remote mgmt/tools/traffic_mcast_snoop diff --git a/build/cjose.m4 b/build/cjose.m4 new file mode 100644 index 00000000000..6bead14cd3e --- /dev/null +++ b/build/cjose.m4 @@ -0,0 +1,47 @@ +dnl -------------------------------------------------------- -*- autoconf -*- +dnl Licensed to the Apache Software Foundation (ASF) under one or more +dnl contributor license agreements. See the NOTICE file distributed with +dnl this work for additional information regarding copyright ownership. +dnl The ASF licenses this file to You under the Apache License, Version 2.0 +dnl (the "License"); you may not use this file except in compliance with +dnl the License. You may obtain a copy of the License at +dnl +dnl http://www.apache.org/licenses/LICENSE-2.0 +dnl +dnl Unless required by applicable law or agreed to in writing, software +dnl distributed under the License is distributed on an "AS IS" BASIS, +dnl WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +dnl See the License for the specific language governing permissions and +dnl limitations under the License. + +dnl +dnl cjose.m4: Trafficserver's cjose autoconf macros +dnl + +dnl +dnl TS_CHECK_CJOSE: look for cjose libraries and headers +dnl + +AC_DEFUN([TS_CHECK_CJOSE], [ +AC_MSG_CHECKING([for --with-cjose]) + AC_ARG_WITH( + [cjose], + [AS_HELP_STRING([--with-cjose=DIR], [use a specific cjose library])], + [ LDFLAGS="$LDFLAGS -L$with_cjose/lib"; + CFLAGS="$CFLAGS -I$with_cjose/include/"; + CPPFLAGS="$CPPFLAGS -I$with_cjose/include/"; + AC_MSG_RESULT([$with_cjose]) + ], + [ AC_MSG_RESULT([no])] + ) + + AC_CHECK_HEADERS([cjose/cjose.h], [ + AC_MSG_CHECKING([whether cjose is dynamic]) + TS_LINK_WITH_FLAGS_IFELSE([-fPIC -lcjose -ljansson -lcrypto],[AC_LANG_PROGRAM( + [#include ], + [(void) cjose_jws_import("", 0, NULL);])], + [AC_MSG_RESULT([yes]); LIBCJOSE=-lcjose], + [AC_MSG_RESULT([no]); LIBCJOSE=-l:libcjose.a]) + ], + [LIBCJOSE=]) +]) diff --git a/build/hiredis.m4 b/build/hiredis.m4 new file mode 100644 index 00000000000..e49ad7925ce --- /dev/null +++ b/build/hiredis.m4 @@ -0,0 +1,93 @@ +dnl -------------------------------------------------------- -*- autoconf -*- +dnl Licensed to the Apache Software Foundation (ASF) under one or more +dnl contributor license agreements. See the NOTICE file distributed with +dnl this work for additional information regarding copyright ownership. +dnl The ASF licenses this file to You under the Apache License, Version 2.0 +dnl (the "License"); you may not use this file except in compliance with +dnl the License. You may obtain a copy of the License at +dnl +dnl http://www.apache.org/licenses/LICENSE-2.0 +dnl +dnl Unless required by applicable law or agreed to in writing, software +dnl distributed under the License is distributed on an "AS IS" BASIS, +dnl WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +dnl See the License for the specific language governing permissions and +dnl limitations under the License. + +dnl +dnl hiredis.m4: Trafficserver's hiredis autoconf macros +dnl + +dnl +dnl TS_CHECK_HIREDIS: look for hiredis libraries and headers +dnl + +AC_DEFUN([TS_CHECK_HIREDIS], [ +hiredis_base_dir='/usr' +has_hiredis=0 +AC_ARG_WITH(hiredis, [AC_HELP_STRING([--with-hiredis=DIR],[use a specific hiredis library])], +[ + has_hiredis=1 + if test "x$withval" != "xyes" && test "x$withval" != "x"; then + hiredis_base_dir="$withval" + if test "$withval" != "no"; then + case "$withval" in + *":"*) + hiredis_include="`echo $withval |sed -e 's/:.*$//'`" + hiredis_ldflags="`echo $withval |sed -e 's/^.*://'`" + AC_MSG_CHECKING(checking for hiredis includes in $hiredis_include libs in $hiredis_ldflags ) + ;; + *) + hiredis_include="$withval/include" + hiredis_ldflags="$withval/lib" + AC_MSG_CHECKING(checking for hiredis includes in $withval) + ;; + esac + fi + fi + + if test -d $hiredis_include && test -d $hiredis_ldflags && test -f $hiredis_include/hiredis/hiredis.h; then + AC_MSG_RESULT([ok]) + else + has_hiredis=0 + AC_MSG_RESULT([not found]) + fi + +if test "$has_hiredis" != "0"; then + saved_ldflags=$LDFLAGS + saved_cppflags=$CPPFLAGS + hiredis_have_headers=0 + hiredis_have_libs=0 + if test "$hiredis_base_dir" != "/usr"; then + TS_ADDTO(CPPFLAGS, [-I${hiredis_include}]) + TS_ADDTO(LDFLAGS, [-L${hiredis_ldflags}]) + TS_ADDTO_RPATH(${hiredis_ldflags}) + fi + + AC_CHECK_LIB([hiredis], redisConnect, [hiredis_have_libs=1]) + if test "$hiredis_have_libs" != "0"; then + AC_CHECK_HEADERS(hiredis/hiredis.h, [hiredis_have_headers=1]) + fi + if test "$hiredis_have_headers" != "0"; then + AC_SUBST([LIB_HIREDIS], [-lhiredis]) + AC_SUBST([CFLAGS_HIREDIS], [-I${hiredis_include}]) + else + has_hiredis=0 + CPPFLAGS=$saved_cppflags + LDFLAGS=$saved_ldflags + fi +fi +], +[ +has_hiredis=1 +AC_CHECK_HEADER([hiredis/hiredis.h], [], [has_hiredis=0]) +AC_CHECK_LIB([hiredis], redisConnect, [], [has_hiredis=0]) + +if test "x$has_hiredis" == "x1"; then + AC_SUBST([LIB_HIREDIS], [-lhiredis]) +fi +]) + +]) + + diff --git a/build/jansson.m4 b/build/jansson.m4 new file mode 100644 index 00000000000..07987c5acf7 --- /dev/null +++ b/build/jansson.m4 @@ -0,0 +1,47 @@ +dnl -------------------------------------------------------- -*- autoconf -*- +dnl Licensed to the Apache Software Foundation (ASF) under one or more +dnl contributor license agreements. See the NOTICE file distributed with +dnl this work for additional information regarding copyright ownership. +dnl The ASF licenses this file to You under the Apache License, Version 2.0 +dnl (the "License"); you may not use this file except in compliance with +dnl the License. You may obtain a copy of the License at +dnl +dnl http://www.apache.org/licenses/LICENSE-2.0 +dnl +dnl Unless required by applicable law or agreed to in writing, software +dnl distributed under the License is distributed on an "AS IS" BASIS, +dnl WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +dnl See the License for the specific language governing permissions and +dnl limitations under the License. + +dnl +dnl jansson.m4: Trafficserver's jansson autoconf macros +dnl + +dnl +dnl TS_CHECK_JANSSON: look for jansson libraries and headers +dnl + +AC_DEFUN([TS_CHECK_JANSSON], [ + AC_MSG_CHECKING([for --with-jansson]) + AC_ARG_WITH( + [jansson], + [AS_HELP_STRING([--with-jansson], [use a specific jansson library])], + [ LDFLAGS="$LDFLAGS -L$with_jansson/lib"; + CFLAGS="$CFLAGS -I$with_jansson/include/"; + CPPFLAGS="$CPPFLAGS -I$with_jansson/include/"; + AC_MSG_RESULT([$with_jansson]) + ], + [ AC_MSG_RESULT([no])] + ) + + AC_CHECK_HEADERS([jansson.h], [ + AC_MSG_CHECKING([whether jansson is dynamic]) + TS_LINK_WITH_FLAGS_IFELSE([-fPIC -ljansson],[AC_LANG_PROGRAM( + [#include ], + [(void) json_object();])], + [AC_MSG_RESULT([yes]); LIBJANSSON=-ljansson], + [AC_MSG_RESULT([no]); LIBJANSSON=-l:libjansson.a]) + ], + [LIBJANSSON=]) +]) diff --git a/configure.ac b/configure.ac index c26c54fe73a..0a19722d2fe 100644 --- a/configure.ac +++ b/configure.ac @@ -1316,27 +1316,15 @@ TS_CHECK_LUAJIT # Enable experimental/uri_singing plugin # This is here, instead of above, because it needs to know if PCRE is available. # -AC_CHECK_HEADERS([jansson.h], [ - AC_MSG_CHECKING([whether jansson is dynamic]) - TS_LINK_WITH_FLAGS_IFELSE([-fPIC -ljansson],[AC_LANG_PROGRAM( - [#include ], - [(void) json_object();])], - [AC_MSG_RESULT([yes]); LIBJANSSON=-ljansson], - [AC_MSG_RESULT([no]); LIBJANSSON=-l:libjansson.a]) - ], - [LIBJANSSON=]) - -AC_CHECK_HEADERS([cjose/cjose.h], [ - AC_MSG_CHECKING([whether cjose is dynamic]) - TS_LINK_WITH_FLAGS_IFELSE([-fPIC -lcjose],[AC_LANG_PROGRAM( - [#include ], - [(void) cjose_jws_import("", 0, NULL);])], - [AC_MSG_RESULT([yes]); LIBCJOSE=-lcjose], - [AC_MSG_RESULT([no]); LIBCJOSE=-l:libcjose.a]) - ], - [LIBCJOSE=]) + +#### Check for optional jansson library (uri_signing) +TS_CHECK_JANSSON + AC_CHECK_LIB([crypto],[HMAC],[has_libcrypto=1],[has_libcrypto=0]) +#### Check for optional cjose library (uri_signing) +TS_CHECK_CJOSE + AM_CONDITIONAL([BUILD_URI_SIGNING_PLUGIN], [test ! -z "${LIBCJOSE}" -a ! -z "${LIBJANSSON}" -a "x${enable_pcre}" = "xyes" -a "x${has_libcrypto}" = "x1"]) AC_SUBST([LIBCJOSE]) AC_SUBST([LIBJANSSON]) diff --git a/plugins/experimental/uri_signing/Makefile.inc b/plugins/experimental/uri_signing/Makefile.inc index 9499479b4a6..7632c492467 100644 --- a/plugins/experimental/uri_signing/Makefile.inc +++ b/plugins/experimental/uri_signing/Makefile.inc @@ -23,6 +23,22 @@ experimental_uri_signing_uri_signing_la_SOURCES = \ experimental/uri_signing/jwt.c \ experimental/uri_signing/match.c \ experimental/uri_signing/parse.c \ + experimental/uri_signing/normalize.c \ experimental/uri_signing/timing.c experimental_uri_signing_uri_signing_la_LIBADD = @LIBJANSSON@ @LIBCJOSE@ @LIBPCRE@ -lm -lcrypto + +check_PROGRAMS += experimental/uri_signing/test_uri_signing + +experimental_uri_signing_test_uri_signing_CPPFLAGS = $(AM_CPPFLAGS) -I$(abs_top_srcdir)/tests/include -DURI_SIGNING_UNIT_TEST +experimental_uri_signing_test_uri_signing_LDADD = @LIBJANSSON@ @LIBCJOSE@ @LIBPCRE@ -lm -lcrypto +experimental_uri_signing_test_uri_signing_SOURCES = \ + experimental/uri_signing/unit_tests/uri_signing_test.cc \ + experimental/uri_signing/jwt.c \ + experimental/uri_signing/common.c \ + experimental/uri_signing/parse.c \ + experimental/uri_signing/cookie.c \ + experimental/uri_signing/config.c \ + experimental/uri_signing/timing.c \ + experimental/uri_signing/normalize.c \ + experimental/uri_signing/match.c diff --git a/plugins/experimental/uri_signing/README.md b/plugins/experimental/uri_signing/README.md index fe242d8d98f..02d7c20f4a2 100644 --- a/plugins/experimental/uri_signing/README.md +++ b/plugins/experimental/uri_signing/README.md @@ -1,8 +1,7 @@ URI Signing Plugin ================== -This remap plugin implements the draft URI Signing protocol documented here: -https://tools.ietf.org/html/draft-ietf-cdni-uri-signing-12 . +This remap plugin implements the draft URI Signing protocol documented [here](https://tools.ietf.org/html/draft-ietf-cdni-uri-signing-16): It takes a single argument: the name of a config file that contains key information. @@ -17,6 +16,8 @@ this plugin gets the URI. Config ------ +### Keys + The config file should be a JSON object that maps issuer names to JWK-sets. Exactly one of these JWK-sets must have an additional member indicating the renewal key. @@ -75,6 +76,33 @@ It's worth noting that multiple issuers can provide `auth_directives`. Each issuer will be processed in order and any issuer can provide access to a path. +### More Configuration Options + +**Strip Token** +When the strip_token parameter is set to true, the plugin removes the +token from both the url that is sent upstream to the origin and the url that +is used as the cache key. The strip_token parameter defaults to false and should +be set by only one issuer. +**ID** +The id field takes a string indicating the identification of the entity processing the request. +This is used in aud claim checks to ensure that the receiver is the intended audience of a +tokenized request. The id parameter can only be set by one issuer. + +Example: + + { + "Kabletown URI Authority": { + "renewal_kid": "Second Key", + "strip_token" : true, + "id" : "mycdn", + "auth_directives": [ + ⋮ + ] + "keys": [ + ⋮ + ] + } + Usage ----- @@ -85,31 +113,34 @@ will receive a 403 Forbidden response, instead of receiving content. Tokens will be found in either of these places: - A query parameter named `URISigningPackage`. The value must be the JWT. + - A path parameter named `URISigningPackage`. The value must be the JWT. - A cookie named `URISigningPackage`. The value of the cookie must be the JWT. -Path parameters will not be searched for JWTs. - ### Supported Claims The following claims are understood: - `iss`: Must be present. The issuer is used to locate the key for verification. - - `sub`: Validated last, after key verification. **Only `uri-regex` is supported!** + - `sub`: May be present, but is not validated. - `exp`: Expired tokens are not valid. + - `nbf`: Tokens processed before this time are not valid. + - `aud`: Token aud claim strings must match the configured id to be considered valid. - `iat`: May be present, but is not validated. - `cdniv`: Must be missing or 1. - - `cdnistt`: If present, must be 1. + - `cdniuc`: Validated last, after key verificationD. **Only `regex` is supported!** - `cdniets`: If cdnistt is 1, this must be present and non-zero. + - `cdnistt`: If present, must be 1. + - `cdnistd`: If present, must be 0. ### Unsupported Claims These claims are not supported. If they are present, the token will not validate: - - `aud` - - `nbf` - `jti` + - `cdnicrit` + - `cdniip` -In addition, the `sub` containers of `uri`, `uri-pattern`, and `uri-hash` are +In addition, the `cdniuc` container of `hash` is **not supported**. ### Token Renewal @@ -147,6 +178,9 @@ This builds in-tree with the rest of the ATS plugins. Of special note, however, are the first two libraries: cjose and jansson. These libraries are not currently used anywhere else, so they may not be installed. +Note that the default prefix value for cjose is /usr/local. Ensure this is visible to +any executables that are being run using this library. + As of this writing, both libraries install a dynamic library and a static archive. However, by default, the static archive is not compiled with Position Independent Code. The build script will detect this and build a dynamic @@ -156,3 +190,42 @@ plugin. If you would like to statically link them, you will need to ensure that they are compiled with the `-fPIC` flag in their CFLAGs. If the archives have PIC, the build scripts will automatically statically link them. + +Here are some sample commands for building jansson, cjose and trafficserver +locally using static linking. This assumes all source is under ${HOME}/git. + +### Sample + +If using local jansson: + + cd ${HOME}/git + git clone https://github.com/akheron/jansson.git + cd jansson + autoreconf -i + ./configure --disable-shared CC="gcc -fpic" + make -j`nproc` + + # Needed for ATS configure + ln -s src/.libs lib + ln -s src include + +If using local cjose: + + cd ${HOME}/git + git clone https://github.com/cisco/cjose.git + cd cjose + autoreconf -i + ./configure --with-jansson=${HOME}/git/jansson --disable-shared CC="gcc -fpic" + make -j`nproc` + + # Needed for ATS configure + ln -s src/.libs lib + +ATS: + + cd ${HOME}/git/ + git clone https://github.com/apache/trafficserver.git + cd trafficserver + autoreconf -i + ./configure --enable-experimental-plugins --with-jansson=${HOME}/git/jansson --with-cjose=${HOME}/git/cjose + make -j`nproc` diff --git a/plugins/experimental/uri_signing/common.c b/plugins/experimental/uri_signing/common.c new file mode 100644 index 00000000000..bae8b6d911a --- /dev/null +++ b/plugins/experimental/uri_signing/common.c @@ -0,0 +1,32 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include "common.h" + +#ifdef URI_SIGNING_UNIT_TEST + +void +PrintToStdErr(const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + vfprintf(stderr, fmt, args); + va_end(args); +} + +#endif diff --git a/plugins/experimental/uri_signing/common.h b/plugins/experimental/uri_signing/common.h new file mode 100644 index 00000000000..9a51bb61b97 --- /dev/null +++ b/plugins/experimental/uri_signing/common.h @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#define PLUGIN_NAME "uri_signing" + +#ifdef URI_SIGNING_UNIT_TEST +#include +#include + +#define PluginDebug(fmt, ...) PrintToStdErr("(%s) %s:%d:%s() " fmt "\n", PLUGIN_NAME, __FILE__, __LINE__, __func__, ##__VA_ARGS__) +#define PluginError(fmt, ...) PrintToStdErr("(%s) %s:%d:%s() " fmt "\n", PLUGIN_NAME, __FILE__, __LINE__, __func__, ##__VA_ARGS__) +#define TSmalloc(x) malloc(x) +#define TSfree(p) free(p) +void PrintToStdErr(const char *fmt, ...); + +#else + +#include "ts/ts.h" +#define __FILENAME__ (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__) +#define PluginDebug(fmt, ...) TSDebug(PLUGIN_NAME, "[%s:% 4d] %s(): " fmt, __FILENAME__, __LINE__, __func__, ##__VA_ARGS__); +#define PluginError(fmt, ...) \ + PluginDebug(fmt, ##__VA_ARGS__); \ + TSError("[%s:% 4d] %s(): " fmt, __FILENAME__, __LINE__, __func__, ##__VA_ARGS__); + +#endif diff --git a/plugins/experimental/uri_signing/config.c b/plugins/experimental/uri_signing/config.c index 83083f8fbda..8727e9f3c98 100644 --- a/plugins/experimental/uri_signing/config.c +++ b/plugins/experimental/uri_signing/config.c @@ -16,13 +16,11 @@ * limitations under the License. */ -#include "uri_signing.h" +#include "common.h" #include "config.h" #include "timing.h" #include "jwt.h" -#include - #include #include @@ -45,6 +43,8 @@ struct config { char **issuer_names; struct signer signer; struct auth_directive *auth_directives; + char *id; + bool strip_token; }; cjose_jwk_t ** @@ -80,6 +80,18 @@ find_key_by_kid(struct config *cfg, const char *issuer, const char *kid) return NULL; } +const char * +config_get_id(struct config *cfg) +{ + return cfg->id; +} + +bool +config_strip_token(struct config *cfg) +{ + return cfg->strip_token; +} + struct config * config_new(size_t n) { @@ -105,6 +117,9 @@ config_new(size_t n) cfg->signer.alg = NULL; cfg->auth_directives = NULL; + cfg->id = NULL; + + cfg->strip_token = false; PluginDebug("New config object created at %p", cfg); return cfg; @@ -117,6 +132,7 @@ config_delete(struct config *cfg) return; } hdestroy_r(cfg->issuers); + free(cfg->issuers); for (cjose_jwk_t ***jwkis = cfg->jwkis; *jwkis; ++jwkis) { for (cjose_jwk_t **jwks = *jwkis; *jwks; ++jwks) { @@ -126,6 +142,10 @@ config_delete(struct config *cfg) } free(cfg->jwkis); + if (cfg->id) { + free(cfg->id); + } + for (char **name = cfg->issuer_names; *name; ++name) { free(*name); } @@ -259,10 +279,26 @@ read_config(const char *path) renewal_kid = json_string_value(renewal_kid_json); } + json_t *id_json = json_object_get(jwks, "id"); + const char *id; + if (id_json) { + id = json_string_value(id_json); + if (id) { + cfg->id = malloc(strlen(id) + 1); + strcpy(cfg->id, id); + PluginDebug("Found Id in the config: %s", cfg->id); + } + } + + json_t *strip_json = json_object_get(jwks, "strip_token"); + if (strip_json) { + cfg->strip_token = json_boolean_value(strip_json); + } + size_t jwks_ct = json_array_size(key_ary); cjose_jwk_t **jwks = (*jwkis++ = malloc((jwks_ct + 1) * sizeof *jwks)); PluginDebug("Created table with size %d", cfg->issuers->size); - if (!hsearch_r(((ENTRY){(char *)*issuer, jwks}), ENTER, &(ENTRY *){0}, cfg->issuers)) { + if (!hsearch_r(((ENTRY){*issuer, jwks}), ENTER, &(ENTRY *){0}, cfg->issuers)) { PluginDebug("Failed to store keys for issuer %s", *issuer); } else { PluginDebug("Stored keys for %s at %16p", *issuer, jwks); diff --git a/plugins/experimental/uri_signing/config.h b/plugins/experimental/uri_signing/config.h index 75a82f24d88..16bb6c9fb24 100644 --- a/plugins/experimental/uri_signing/config.h +++ b/plugins/experimental/uri_signing/config.h @@ -16,6 +16,8 @@ * limitations under the License. */ +#pragma once + #include #include @@ -33,3 +35,5 @@ struct signer *config_signer(struct config *); struct _cjose_jwk_int **find_keys(struct config *cfg, const char *issuer); struct _cjose_jwk_int *find_key_by_kid(struct config *cfg, const char *issuer, const char *kid); bool uri_matches_auth_directive(struct config *cfg, const char *uri, size_t uri_ct); +const char *config_get_id(struct config *cfg); +bool config_strip_token(struct config *cfg); diff --git a/plugins/experimental/uri_signing/cookie.c b/plugins/experimental/uri_signing/cookie.c index 70dd1aa8469..1e9fc7f9a30 100644 --- a/plugins/experimental/uri_signing/cookie.c +++ b/plugins/experimental/uri_signing/cookie.c @@ -17,8 +17,7 @@ */ #include "cookie.h" -#include "uri_signing.h" -#include +#include "common.h" #include const char * diff --git a/plugins/experimental/uri_signing/cookie.h b/plugins/experimental/uri_signing/cookie.h index a5d0f20b4db..1944df9b4cc 100644 --- a/plugins/experimental/uri_signing/cookie.h +++ b/plugins/experimental/uri_signing/cookie.h @@ -16,5 +16,7 @@ * limitations under the License. */ +#pragma once + #include const char *get_cookie_value(const char **cookie, size_t *cookie_ct, const char *key, size_t *ct); diff --git a/plugins/experimental/uri_signing/jwt.c b/plugins/experimental/uri_signing/jwt.c index d509659ec7d..f14ecb6e289 100644 --- a/plugins/experimental/uri_signing/jwt.c +++ b/plugins/experimental/uri_signing/jwt.c @@ -16,10 +16,10 @@ * limitations under the License. */ -#include "uri_signing.h" +#include "common.h" #include "jwt.h" #include "match.h" -#include "ts/ts.h" +#include "normalize.h" #include #include #include @@ -55,14 +55,18 @@ parse_jwt(json_t *raw) jwt->raw = raw; jwt->iss = json_string_value(json_object_get(raw, "iss")); jwt->sub = json_string_value(json_object_get(raw, "sub")); - jwt->aud = json_string_value(json_object_get(raw, "aud")); + jwt->aud = json_object_get(raw, "aud"); jwt->exp = parse_number(json_object_get(raw, "exp")); jwt->nbf = parse_number(json_object_get(raw, "nbf")); jwt->iat = parse_number(json_object_get(raw, "iat")); jwt->jti = json_string_value(json_object_get(raw, "jti")); jwt->cdniv = parse_integer_default(json_object_get(raw, "cdniv"), 1); + jwt->cdnicrit = json_string_value(json_object_get(raw, "cdnicrit")); + jwt->cdniip = json_string_value(json_object_get(raw, "cdniip")); + jwt->cdniuc = json_string_value(json_object_get(raw, "cdniuc")); jwt->cdniets = json_integer_value(json_object_get(raw, "cdniets")); jwt->cdnistt = json_integer_value(json_object_get(raw, "cdnistt")); + jwt->cdnistd = parse_integer_default(json_object_get(raw, "cdnistd"), 0); return jwt; } @@ -72,6 +76,8 @@ jwt_delete(struct jwt *jwt) if (!jwt) { return; } + + json_decref(jwt->aud); json_decref(jwt->raw); free(jwt); } @@ -92,12 +98,6 @@ unsupported_string_claim(const char *str) return !str; } -bool -unsupported_date_claim(double t) -{ - return isnan(t); -} - bool jwt_validate(struct jwt *jwt) { @@ -111,28 +111,28 @@ jwt_validate(struct jwt *jwt) return false; } - if (!jwt->sub) { /* Mandatory claim. Will be validated after key verification. */ - PluginDebug("Initial JWT Failure: missing sub"); + if (now() > jwt->exp) { + PluginDebug("Initial JWT Failure: expired token"); return false; } - if (!unsupported_string_claim(jwt->aud)) { - PluginDebug("Initial JWT Failure: missing sub"); + if (now() < jwt->nbf) { + PluginDebug("Initial JWT Failure: nbf claim violated"); return false; } - if (now() > jwt->exp) { - PluginDebug("Initial JWT Failure: expired token"); + if (!unsupported_string_claim(jwt->cdniip)) { + PluginDebug("Initial JWT Failure: cdniip unsupported"); return false; } - if (!unsupported_date_claim(jwt->nbf)) { - PluginDebug("Initial JWT Failure: nbf unsupported"); + if (!unsupported_string_claim(jwt->jti)) { + PluginDebug("Initial JWT Failure: nonse unsupported"); return false; } - if (!unsupported_string_claim(jwt->jti)) { - PluginDebug("Initial JWT Failure: nonse unsupported"); + if (!unsupported_string_claim(jwt->cdnicrit)) { + PluginDebug("Initial JWT Failure: cdnicrit unsupported"); return false; } @@ -141,52 +141,113 @@ jwt_validate(struct jwt *jwt) return false; } + if (jwt->cdnistd != 0) { + PluginDebug("Initial JWT Failure: unsupported value for cdnistd: %d", jwt->cdnistd); + return false; + } + return true; } bool -jwt_check_uri(const char *sub, const char *uri) +jwt_check_aud(json_t *aud, const char *id) +{ + if (!aud) { + return true; + } + if (!id) { + return false; + } + /* If aud is a string, do a simple string comparison */ + const char *aud_str = json_string_value(aud); + if (aud_str) { + PluginDebug("Checking aud %s agaisnt token aud string \"%s\"", id, aud_str); + /* Both strings will be null terminated per jansson docs */ + if (strcmp(aud_str, id) == 0) { + return true; + } + return false; + } + PluginDebug("Checking aud %s agaisnt token aud array", id); + /* If aud is an array, check all items */ + if (json_is_array(aud)) { + size_t index; + json_t *aud_item; + json_array_foreach(aud, index, aud_item) + { + aud_str = json_string_value(aud_item); + if (aud_str) { + if (strcmp(aud_str, id) == 0) { + return true; + } + } + } + } + return false; +} + +bool +jwt_check_uri(const char *cdniuc, const char *uri) { - static const char CONT_URI_STR[] = "uri"; - static const char CONT_URI_PATTERN_STR[] = "uri-pattern"; - static const char CONT_URI_REGEX_STR[] = "uri-regex"; + static const char CONT_URI_HASH_STR[] = "hash"; + static const char CONT_URI_REGEX_STR[] = "regex"; + + /* If cdniuc is not provided, skip uri check */ + if (!cdniuc || !*cdniuc) { + return true; + } - if (!sub || !*sub || !uri) { + if (!uri) { return false; } - const char *kind = sub, *container = sub; + /* Normalize the URI */ + int uri_ct = strlen(uri); + int buff_ct = uri_ct + 2; + int err; + char *normal_uri = (char *)TSmalloc(buff_ct); + memset(normal_uri, 0, buff_ct); + + err = normalize_uri(uri, uri_ct, normal_uri, buff_ct); + + if (err) { + goto fail_jwt; + } + + const char *kind = cdniuc, *container = cdniuc; while (*container && *container != ':') { ++container; } if (!*container) { - return false; + goto fail_jwt; } ++container; size_t len = container - kind; - PluginDebug("Comparing with match kind \"%.*s\" on \"%s\" to \"%s\"", (int)len - 1, kind, container, uri); + bool status; + PluginDebug("Comparing with match kind \"%.*s\" on \"%s\" to normalized URI \"%s\"", (int)len - 1, kind, container, normal_uri); switch (len) { - case sizeof CONT_URI_STR: - if (!strncmp(CONT_URI_STR, kind, len - 1)) { - return !strcmp(container, uri); + case sizeof CONT_URI_HASH_STR: + if (!strncmp(CONT_URI_HASH_STR, kind, len - 1)) { + status = match_hash(container, normal_uri); + TSfree(normal_uri); + return status; } - PluginDebug("Expected kind %s, but did not find it in \"%.*s\"", CONT_URI_STR, (int)len - 1, kind); - break; - case sizeof CONT_URI_PATTERN_STR: - if (!strncmp(CONT_URI_PATTERN_STR, kind, len - 1)) { - return match_glob(container, uri); - } - PluginDebug("Expected kind %s, but did not find it in \"%.*s\"", CONT_URI_PATTERN_STR, (int)len - 1, kind); + PluginDebug("Expected kind %s, but did not find it in \"%.*s\"", CONT_URI_HASH_STR, (int)len - 1, kind); break; case sizeof CONT_URI_REGEX_STR: if (!strncmp(CONT_URI_REGEX_STR, kind, len - 1)) { - return match_regex(container, uri); + status = match_regex(container, normal_uri); + TSfree(normal_uri); + return status; } PluginDebug("Expected kind %s, but did not find it in \"%.*s\"", CONT_URI_REGEX_STR, (int)len - 1, kind); break; } PluginDebug("Unknown match kind \"%.*s\"", (int)len - 1, kind); + +fail_jwt: + TSfree(normal_uri); return false; } @@ -198,6 +259,14 @@ renew_copy_string(json_t *new_json, const char *name, const char *old) } } +void +renew_copy_raw(json_t *new_json, const char *name, json_t *old_json) +{ + if (old_json) { + json_object_set_new(new_json, name, old_json); + } +} + void renew_copy_real(json_t *new_json, const char *name, double old) { @@ -230,16 +299,19 @@ renew(struct jwt *jwt, const char *iss, cjose_jwk_t *jwk, const char *alg, const json_t *new_json = json_object(); renew_copy_string(new_json, "iss", iss); /* use issuer of new signing key */ renew_copy_string(new_json, "sub", jwt->sub); - renew_copy_string(new_json, "aud", jwt->aud); + renew_copy_raw(new_json, "aud", jwt->aud); renew_copy_real(new_json, "exp", now() + jwt->cdniets); /* expire ets seconds hence */ renew_copy_real(new_json, "nbf", jwt->nbf); renew_copy_real(new_json, "iat", now()); /* issued now */ renew_copy_string(new_json, "jti", jwt->jti); + renew_copy_string(new_json, "cdniuc", jwt->cdniuc); renew_copy_integer(new_json, "cdniv", jwt->cdniv); renew_copy_integer(new_json, "cdniets", jwt->cdniets); renew_copy_integer(new_json, "cdnistt", jwt->cdnistt); + renew_copy_integer(new_json, "cdnistd", jwt->cdnistd); char *pt = json_dumps(new_json, JSON_COMPACT); + json_decref(new_json); cjose_header_t *hdr = cjose_header_new(NULL); if (!hdr) { diff --git a/plugins/experimental/uri_signing/jwt.h b/plugins/experimental/uri_signing/jwt.h index 786e6b9db13..1e4d58fefda 100644 --- a/plugins/experimental/uri_signing/jwt.h +++ b/plugins/experimental/uri_signing/jwt.h @@ -16,6 +16,8 @@ * limitations under the License. */ +#pragma once + #include #include @@ -23,19 +25,24 @@ struct jwt { json_t *raw; const char *iss; const char *sub; - const char *aud; + json_t *aud; double exp; double nbf; double iat; const char *jti; int cdniv; + const char *cdnicrit; + const char *cdniip; + const char *cdniuc; int cdniets; int cdnistt; + int cdnistd; }; struct jwt *parse_jwt(json_t *raw); void jwt_delete(struct jwt *jwt); bool jwt_validate(struct jwt *jwt); -bool jwt_check_uri(const char *sub, const char *uri); +bool jwt_check_aud(json_t *aud, const char *id); +bool jwt_check_uri(const char *cdniuc, const char *uri); struct _cjose_jwk_int; char *renew(struct jwt *jwt, const char *iss, struct _cjose_jwk_int *jwk, const char *alg, const char *package); diff --git a/plugins/experimental/uri_signing/match.c b/plugins/experimental/uri_signing/match.c index ad376a251e4..faea8953dfc 100644 --- a/plugins/experimental/uri_signing/match.c +++ b/plugins/experimental/uri_signing/match.c @@ -16,14 +16,13 @@ * limitations under the License. */ -#include "uri_signing.h" -#include "ts/ts.h" +#include +#include "common.h" #include -#include #include bool -match_glob(const char *needle, const char *haystack) +match_hash(const char *needle, const char *haystack) { return false; } @@ -31,16 +30,27 @@ match_glob(const char *needle, const char *haystack) bool match_regex(const char *pattern, const char *uri) { - const char *err; - int err_off; + struct re_pattern_buffer pat_buff; + + pat_buff.translate = 0; + pat_buff.fastmap = 0; + pat_buff.buffer = 0; + pat_buff.allocated = 0; + + re_syntax_options = RE_SYNTAX_POSIX_MINIMAL_EXTENDED; + PluginDebug("Testing regex pattern /%s/ against \"%s\"", pattern, uri); - pcre *re = pcre_compile(pattern, PCRE_ANCHORED | PCRE_UCP | PCRE_UTF8, &err, &err_off, NULL); - if (!re) { - PluginDebug("Regex /%s/ failed to compile.", pattern); + + const char *comp_err = re_compile_pattern(pattern, strlen(pattern), &pat_buff); + + if (comp_err) { + PluginDebug("Regex Compilation ERROR: %s", comp_err); return false; } - int rc = pcre_exec(re, NULL, uri, strlen(uri), 0, 0, NULL, 0); - pcre_free(re); - return rc >= 0; + int match_ret; + match_ret = re_match(&pat_buff, uri, strlen(uri), 0, 0); + regfree(&pat_buff); + + return match_ret >= 0; } diff --git a/plugins/experimental/uri_signing/match.h b/plugins/experimental/uri_signing/match.h index 92b906dbd35..38f3eb28e83 100644 --- a/plugins/experimental/uri_signing/match.h +++ b/plugins/experimental/uri_signing/match.h @@ -17,5 +17,5 @@ */ #include -bool match_glob(const char *needle, const char *haystack); +bool match_hash(const char *needle, const char *haystack); bool match_regex(const char *pattern, const char *uri); diff --git a/plugins/experimental/uri_signing/normalize.c b/plugins/experimental/uri_signing/normalize.c new file mode 100644 index 00000000000..cd475868e2e --- /dev/null +++ b/plugins/experimental/uri_signing/normalize.c @@ -0,0 +1,382 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "normalize.h" +#include "common.h" +#include +#include +#include +#include + +/* Remove Dot Algorithm outlined in RFC3986 section 5.2.4 + * Function writes normalizes path and writes to ret_buffer */ +int +remove_dot_segments(const char *path, int path_ct, char *ret_buffer, int buff_ct) +{ + /* Ensure buffer is at least the size of the path */ + if (buff_ct < path_ct) { + PluginDebug("Path buffer not large enough"); + return -1; + } + + /* Create an input buffer that we can change */ + char inBuff[path_ct + 1]; + memset(inBuff, 0, path_ct + 1); + strcpy(inBuff, path); + + const char *path_end = inBuff + path_ct; + char *seg_start = inBuff; + char *seg_end; + char *write_buffer = ret_buffer; + int seg_len; + + for (;;) { + if (seg_start == path_end) { + break; + } + seg_end = seg_start + 1; + + /* Parse such that Seg start/end contain the next full path segment */ + while (seg_end != path_end && *seg_end != '/') { + seg_end++; + } + + seg_len = seg_end - seg_start + 1; + + /* Remove starting ../ or ./ from input buffer */ + if (!strncmp(seg_start, "../", seg_len) || !strncmp(seg_start, "./", seg_len)) { + if (seg_end != path_end) { + seg_end++; + } + } + + /* Remove starting /./ or /. from input buffer and replace with '/' in output buffer */ + else if (!strncmp(seg_start, "/./", seg_len) || !strncmp(seg_start, "/.", seg_len)) { + *write_buffer = '/'; + write_buffer++; + if (seg_end != path_end) { + seg_end++; + } + } + + /* Replace /../ or /.. with / in write_buffer and remove preceding segment */ + else if (!strncmp(seg_start, "/../", seg_len) || !strncmp(seg_start, "/..", seg_len)) { + int prev_len = 0; + while (*write_buffer != '/' && write_buffer != ret_buffer) { + prev_len++; + write_buffer--; + } + memset(write_buffer, 0, prev_len); + + /* Replace segment with '/' in input buffer */ + if (seg_end != path_end) { + seg_start[seg_len - 1] = '/'; + } else { + seg_start[seg_len - 2] = '/'; + seg_end--; + } + } + + /* Remove starting '.' or '..' from input buffer */ + else if (!strncmp(seg_start, ".", seg_len) || !strncmp(seg_start, "..", seg_len)) { + if (seg_end != path_end) { + seg_end++; + } + } + /* Place the current path segment to the output buffer including initial '/' but not the next '/' */ + else { + /* Write first forward slash to buffer */ + if (*seg_start == '/') { + *write_buffer = *seg_start; + write_buffer++; + seg_start++; + } + + /* Write subsequent characters to buffer */ + while (*seg_start != '/') { + *write_buffer = *seg_start; + write_buffer++; + if (*seg_start == 0) { + break; + } + seg_start++; + } + } + seg_start = seg_end; + } + + PluginDebug("Normalized Path: %s", ret_buffer); + return strlen(ret_buffer); +} + +/* Function percent decodes uri_ct characters of the string uri and writes it to the decoded_uri + * buffer. If lower is true, it sets all characters including decoded ones to lower case. + * The function returns the length of the decoded string or -1 if there was a parsing error + * TODO: ADD functionality to ignore unicode non-standard characters and leave them encoded. Read RFC regarding normalization and + * determine if this is compliant. + */ +int +percent_decode(const char *uri, int uri_ct, char *decoded_uri, bool lower) +{ + static const char *reserved_string = ":/?#[]@!$&\'()*+,;="; + + if (uri_ct <= 0) { + return 0; + } + + int offset = 0; + int i; + for (i = 0; i < uri_ct; i++) { + if (uri[i] == '%') { + /* The next two characters are interpreted as the hex encoded value. Store in encodedVal */ + if (uri_ct < i + 2) { + goto decode_failure; + } + char encodedVal[2] = {0}; + int j; + for (j = 0; j < 2; j++) { + if (isxdigit(uri[i + j + 1])) { + encodedVal[j] = uri[i + j + 1]; + } else { + goto decode_failure; + } + } + int hexVal = 0; + char decodeChar; + sscanf(encodedVal, "%2x", &hexVal); + decodeChar = (char)hexVal; + /* If encoded value is a reserved char, leave encoded*/ + if (strchr(reserved_string, decodeChar)) { + decoded_uri[i - offset] = uri[i]; + decoded_uri[i + 1 - offset] = toupper(uri[i + 1]); + decoded_uri[i + 2 - offset] = toupper(uri[i + 2]); + } + /* If not a reserved char, decode using the decoded_uri buffer */ + else { + if (lower) { + decoded_uri[i - offset] = tolower(decodeChar); + } else { + decoded_uri[i - offset] = decodeChar; + } + offset = offset + 2; + } + i = i + 2; + } + /* Write non-encoded values to decoded buffer */ + else { + if (lower) { + decoded_uri[i - offset] = tolower(uri[i]); + } else { + decoded_uri[i - offset] = uri[i]; + } + } + } + + /* Return the size of the newly decoded string */ + return uri_ct - offset; + +decode_failure: + PluginDebug("ERROR Decoding URI"); + return -1; +} + +/* This function takes a uri and an initialized buffer to populate with the normalized uri. + * Returns non zero for error + * + * The buffer provided must be at least the length of the uri + 1 as the normalized uri will + * potentially be one char larger than the original uri if a backslash is added to the path. + * + * The normalization function returns a string with the following modifications + * 1. Lowecase protocol/domain + * 2. Path segments .. and . are removed from path + * 3. Alphabetical percent encoded octet values are toupper + * 4. Non-reserved percent encoded octet values are decoded + * 5. The Port is removed if it is default + * 6. Defaults to a single backslash for the path segment if path segment is empty + */ +int +normalize_uri(const char *uri, int uri_ct, char *normal_uri, int normal_ct) +{ + PluginDebug("Normalizing URI: %s", uri); + + /* Buffer provided must be large enough to store the uri plus one additional char */ + const char *uri_end = uri + uri_ct; + const char *buff_end = normal_uri + normal_ct; + + if ((normal_uri == NULL) || (normal_uri && normal_ct < uri_ct + 1)) { + PluginDebug("Buffer to Normalize URI not large enough."); + return -1; + } + + /* Initialize a path buffer to pass to path normalization function later on */ + char path_buffer[normal_ct]; + memset(path_buffer, 0, normal_ct); + + /* Comp variables store starting/ending indexes for each uri component as uri is parsed. + * Write buffer traverses the normalized uri buffer as we build the normalized string. + */ + const char *comp_start = uri; + const char *comp_end = uri; + char *write_buffer = normal_uri; + bool https = false; + + /* Parse the protocol which will end with a colon */ + while (*comp_end != ':' && comp_end != uri_end) { + *write_buffer = tolower(*comp_end); + comp_end++; + write_buffer++; + } + + if (comp_end == uri_end) { + PluginDebug("Reached End of String prematurely"); + goto normalize_failure; + } + + /* Copy the colon */ + *write_buffer = *comp_end; + comp_end++; + write_buffer++; + + /* Ensure the protocol is either http or https */ + if (strcmp("https:", normal_uri) == 0) { + https = true; + } else if (strcmp("http:", normal_uri)) { + PluginDebug("String is neither http or https"); + goto normalize_failure; + } + + /* Protocol must be terminated by two forward slashes */ + int i; + for (i = 0; i < 2; i++) { + if (comp_end == uri_end || *comp_end != '/') { + goto normalize_failure; + } + *write_buffer = *comp_end; + comp_end++; + write_buffer++; + } + + if (comp_end == uri_end) { + goto normalize_failure; + } + + /* Comp_start is index of start of authority component */ + int comp_ct; + comp_start = comp_end; + + /* Set comp start/end to contain authority component */ + bool userInfo = false; + while (comp_end != uri_end && *comp_end != '/' && *comp_end != '?' && *comp_end != '#') { + /* If we encounter userinfo, decode it without altering case and set comp_start/end to only include hostname/port */ + if (*comp_end == '@' && userInfo == false) { + comp_ct = comp_end - comp_start; + comp_ct = percent_decode(comp_start, comp_ct, write_buffer, false); + if (comp_ct < 0) { + goto normalize_failure; + } + comp_start = comp_end; + userInfo = true; + write_buffer = write_buffer + comp_ct; + } + comp_end++; + } + + /* UserInfo without a hostname is invalid */ + if (userInfo == true && comp_end == uri_end) { + goto normalize_failure; + } + + comp_ct = comp_end - comp_start; + + /* - comp start/end holds indices in original uri of hostname/port + * - write_buffer holds pointer to start of hostname/port written to the decode buffer + * - comp_ct holds size of hostname/port in original uri + */ + + /* Parse and decode the hostname and port and set to lower case */ + comp_ct = percent_decode(comp_start, comp_ct, write_buffer, true); + + if (comp_ct < 0) { + goto normalize_failure; + } + + /* Remove the port from the buffer if default */ + while (*write_buffer != 0) { + if (*write_buffer == ':') { + if (https == true && !strncmp(write_buffer, ":443", 5)) { + memset(write_buffer, 0, 4); + break; + } else if (https == false && !strncmp(write_buffer, ":80", 4)) { + memset(write_buffer, 0, 3); + break; + } + } + write_buffer++; + } + + comp_start = comp_end; + + /* If we have reached the end of the authority section with an empty path component, add a trailing backslash */ + if (*comp_end == 0 || *comp_end == '?' || *comp_end == '#') { + *write_buffer = '/'; + write_buffer++; + } + + /* If there is a path component, normalize it */ + else { + /* Set comp start/end pointers to contain the path component */ + while (*comp_end != '?' && *comp_end != '#' && *comp_end != 0) { + comp_end++; + } + /* Decode the path component without altering case and store it to the path_buffer*/ + comp_ct = comp_end - comp_start; + comp_ct = percent_decode(comp_start, comp_ct, path_buffer, false); + + if (comp_ct < 0) { + goto normalize_failure; + } + + /* Remove the . and .. segments from the path and write the now normalized path to the output buffer */ + PluginDebug("Removing Dot Segments"); + int buff_ct = buff_end - write_buffer; + comp_ct = remove_dot_segments(path_buffer, comp_ct, write_buffer, buff_ct); + + if (comp_ct < 0) { + PluginDebug("Failure removing dot segments from path"); + goto normalize_failure; + } + write_buffer = write_buffer + comp_ct; + } + + /* If there is any uri remaining after the path, decode and set case to lower */ + if (comp_end != uri_end) { + comp_start = comp_end; + comp_ct = uri_end - comp_start; + comp_ct = percent_decode(comp_start, comp_ct, write_buffer, false); + if (comp_ct < 0) { + goto normalize_failure; + } + } + + PluginDebug("Normalized URI: %s", normal_uri); + return 0; + +normalize_failure: + PluginDebug("URI Normalization Failure. URI does not fit http or https schemes."); + return -1; +} diff --git a/plugins/experimental/uri_signing/uri_signing.h b/plugins/experimental/uri_signing/normalize.h similarity index 80% rename from plugins/experimental/uri_signing/uri_signing.h rename to plugins/experimental/uri_signing/normalize.h index 6cb5046c2df..ced84bf635c 100644 --- a/plugins/experimental/uri_signing/uri_signing.h +++ b/plugins/experimental/uri_signing/normalize.h @@ -16,7 +16,7 @@ * limitations under the License. */ -#define PLUGIN_NAME "uri_signing" +#pragma once -#define PluginDebug(...) TSDebug("uri_signing", PLUGIN_NAME " " __VA_ARGS__) -#define PluginError(...) PluginDebug(__VA_ARGS__), TSError(PLUGIN_NAME " " __VA_ARGS__) +int normalize_uri(const char *uri, int uri_ct, char *uri_normal, int buffer_size); +int remove_dot_segments(const char *path, int path_ct, char *ret_buffer, int buff_ct); diff --git a/plugins/experimental/uri_signing/parse.c b/plugins/experimental/uri_signing/parse.c index ade5706a42d..f577e740a7e 100644 --- a/plugins/experimental/uri_signing/parse.c +++ b/plugins/experimental/uri_signing/parse.c @@ -16,7 +16,7 @@ * limitations under the License. */ -#include "uri_signing.h" +#include "common.h" #include "parse.h" #include "config.h" #include "jwt.h" @@ -25,41 +25,68 @@ #include #include #include -#include #include cjose_jws_t * -get_jws_from_query(const char *uri, size_t uri_ct, const char *paramName) +get_jws_from_uri(const char *uri, size_t uri_ct, const char *paramName, char *strip_uri, size_t buff_ct, size_t *strip_ct) { - PluginDebug("Parsing JWS from query string: %.*s", (int)uri_ct, uri); - const char *query = uri; - const char *end = uri + uri_ct; - while (query != end && *query != '?') { - ++query; - } - if (query == end) { + /* Reserved characters as defined by the URI Generic Syntax RFC: https://tools.ietf.org/html/rfc3986#section-2.2 */ + static char const *const reserved_string = ":/?#[]@!$&\'()*+,;="; + static char const *const sub_delim_string = "!$&\'()*+,;="; + + /* If param name ends in reserved character this will be treated as the termination symbol when parsing for package. Default is + * '='. */ + char termination_symbol; + size_t termination_ct; + size_t param_ct = strlen(paramName); + + if (param_ct <= 0) { + PluginDebug("URI signing package name cannot be empty"); return NULL; } - ++query; + if (strchr(reserved_string, paramName[param_ct - 1])) { + termination_symbol = paramName[param_ct - 1]; + termination_ct = param_ct - 1; + } else { + termination_symbol = '='; + termination_ct = param_ct; + } + + PluginDebug("Parsing JWS from query string: %.*s", (int)uri_ct, uri); + const char *param = uri; + const char *end = uri + uri_ct; + const char *key, *key_end; + const char *value, *value_end; - const char *key = query, *key_end; - const char *value = query, *value_end; for (;;) { - while (value != end && *value != '=') { - ++value; + /* Search the URI for a reserved character. */ + while (param != end && strchr(reserved_string, *param) == NULL) { + ++param; } + if (param == end) { + break; + } + + ++param; + /* Parse the parameter for a key value pair separated by the termination symbol. */ + key = param; + value = param; + while (value != end && *value != termination_symbol) { + ++value; + } if (value == end) { break; } - key_end = value; - value_end = ++value; - while (value_end != end && *value_end != '&') { - ++value_end; - } + key_end = value; - if (!strncmp(paramName, key, (size_t)(key_end - key))) { + /* If the Parameter key is our target parameter name, attempt to import a JWS from the value. */ + if ((size_t)(key_end - key) == termination_ct && !strncmp(paramName, key, (size_t)(key_end - key))) { + value_end = ++value; + while (value_end != end && strchr(reserved_string, *value_end) == NULL) { + ++value_end; + } PluginDebug("Decoding JWS: %.*s", (int)(key_end - key), key); cjose_err err = {0}; cjose_jws_t *jws = cjose_jws_import(value, (size_t)(value_end - value), &err); @@ -67,15 +94,32 @@ get_jws_from_query(const char *uri, size_t uri_ct, const char *paramName) PluginDebug("Unable to read JWS: %.*s, %s", (int)(key_end - key), key, err.message ? err.message : ""); } else { PluginDebug("Parsed JWS: %.*s (%16p)", (int)(key_end - key), key, jws); + + /* Strip token */ + /* Check that passed buffer is large enough */ + *strip_ct = ((key - uri) + (end - value_end)); + if (buff_ct <= *strip_ct) { + PluginDebug("Strip URI buffer is not large enough"); + return NULL; + } + + if (value_end != end && strchr(sub_delim_string, *value_end)) { + /*Strip from first char of package name to sub-delimeter that terminates the signed JWT */ + memcpy(strip_uri, uri, (key - uri)); + memcpy(strip_uri + (key - uri), value_end + 1, (end - value_end + 1)); + } else { + /*Strip from reserved char to the last char of the JWT */ + memcpy(strip_uri, uri, (key - uri - 1)); + memcpy(strip_uri + (key - uri - 1), value_end, (end - value_end)); + } + + if (strip_uri[*strip_ct - 1] != '\0') { + strip_uri[*strip_ct - 1] = '\0'; + } + PluginDebug("Stripped URI: %s", strip_uri); } return jws; } - - if (value_end == end) { - break; - } - - key = value = value_end + 1; } PluginDebug("Unable to locate signing key in uri: %.*s", (int)uri_ct, uri); return NULL; @@ -142,7 +186,7 @@ validate_jws(cjose_jws_t *jws, struct config *cfg, const char *uri, size_t uri_c PluginDebug("Initial validation of JWT failed for %16p", jws); goto jwt_fail; } - TimerDebug("inital validation of jwt"); + TimerDebug("initial validation of jwt"); cjose_header_t *hdr = cjose_jws_get_protected(jws); TimerDebug("getting header of jws"); @@ -184,7 +228,12 @@ validate_jws(cjose_jws_t *jws, struct config *cfg, const char *uri, size_t uri_c } } - if (!jwt_check_uri(jwt->sub, uri)) { + if (!jwt_check_aud(jwt->aud, config_get_id(cfg))) { + PluginDebug("Valid key for %16p that does not match aud.", jws); + goto jwt_fail; + } + + if (!jwt_check_uri(jwt->cdniuc, uri)) { PluginDebug("Valid key for %16p that does not match uri.", jws); goto jwt_fail; } diff --git a/plugins/experimental/uri_signing/parse.h b/plugins/experimental/uri_signing/parse.h index 8002f8781fe..98a35abcc28 100644 --- a/plugins/experimental/uri_signing/parse.h +++ b/plugins/experimental/uri_signing/parse.h @@ -16,10 +16,15 @@ * limitations under the License. */ +#pragma once + #include struct _cjose_jws_int; -struct _cjose_jws_int *get_jws_from_query(const char *uri, size_t uri_ct, const char *paramName); + +/* For now strip_ct returns size of string *including* the null terminator */ +struct _cjose_jws_int *get_jws_from_uri(const char *uri, size_t uri_ct, const char *paramName, char *strip_uri, size_t buff_ct, + size_t *strip_ct); struct _cjose_jws_int *get_jws_from_cookie(const char **cookie, size_t *cookie_ct, const char *paramName); struct config; diff --git a/plugins/experimental/uri_signing/python_signer/README.md b/plugins/experimental/uri_signing/python_signer/README.md new file mode 100644 index 00000000000..daa41bb8f2c --- /dev/null +++ b/plugins/experimental/uri_signing/python_signer/README.md @@ -0,0 +1,36 @@ +Python URI Signer +================== + +Given a configuration file and a URI, this python script will generate a signed URI according to the URI signing protocol outlined [here](https://tools.ietf.org/html/draft-ietf-cdni-uri-signing-16): + +The script takes a config file and a uri as command line arguments. It picks one of the keys located in the json file at random +and embeds a valid JWT as a query string parameter into the uri and prints this new signed URI to standard out. + +** Disclaimer ** +Please note that this script is provided as a very simple example of how to implement a signer should not be considered production ready. + +Requirements +------ + +[python-jose](https://pypi.org/project/python-jose/) library must be installed (pip install python-jose). + +Config +------ + +The config file should be a JSON object that contains the following: + + - `iss`: A string representing the issuer of the token + - `token_lifetime`: The lifetime of the token in seconds. Expiry of the token is calculated as now + token_lifetime + - `aud`: A string representing the intended audience of the token. + - `cdnistt`: Boolean value which if set to true uses cookie signed token transport, allowing the validator of the token to + to issue subsequent tokens via set cookie headers. + - `cdniets`: Must be set if using cdnistt. Provides means of setting Expiry Times when generating subsequent tokens. It denotes + the number of seconds to be added to the time at which the JWT is verified that gives the value of the Expiry Time claim of the + next signed JWT. + - `keys`: A list of json objects, each one representing a key. Each key should have the following attributes: + - `alg`: The Cryptographic algorithm to be used with the key. + - `kid`: The key identifier + - `kty`: The key type + - `k`: The key itself + +example_config.json can be used as a template for the configuration file. diff --git a/plugins/experimental/uri_signing/python_signer/example_config.json b/plugins/experimental/uri_signing/python_signer/example_config.json new file mode 100644 index 00000000000..4039796ebad --- /dev/null +++ b/plugins/experimental/uri_signing/python_signer/example_config.json @@ -0,0 +1,33 @@ +{ + "iss": "Example Issuer", + "token_lifetime": 90, + "aud": "Caching Software", + "cdnistt": true, + "cdniets": 30, + "keys": [ + { + "alg": "HS256", + "kid": 0, + "kty": "oct", + "k": "SECRET1" + }, + { + "alg": "HS256", + "kid": 1, + "kty": "oct", + "k": "SECRET2" + }, + { + "alg": "HS256", + "kid": 2, + "kty": "oct", + "k": "SECRET3" + }, + { + "alg": "HS256", + "kid": 3, + "kty": "oct", + "k": "SECRET4" + } + ] +} diff --git a/plugins/experimental/uri_signing/python_signer/uri_signer.py b/plugins/experimental/uri_signing/python_signer/uri_signer.py new file mode 100755 index 00000000000..c30710d4d66 --- /dev/null +++ b/plugins/experimental/uri_signing/python_signer/uri_signer.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import argparse +import random +import time + +# https://github.com/mpdavis/python-jose +from jose import jwt + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('-c', '--config', + help="Configuration File", + required=True) + parser.add_argument('-u', '--uri', + help="URI to sign", + required=True) + + # helpers + parser.add_argument('--key_index', type=int, nargs=1) + parser.add_argument('--token_lifetime', type=int, nargs=1) + + # override arguments -- claims + parser.add_argument('--aud', nargs=1) + parser.add_argument('--cdniets', type=int, nargs=1) + parser.add_argument('--cdnistd', type=int, nargs=1) + parser.add_argument('--cdnistt', type=int, nargs=1) + parser.add_argument('--exp', type=int, nargs=1) + parser.add_argument('--iss', nargs=1) + + # override arguments -- key + parser.add_argument('--alg', nargs=1) + parser.add_argument('--k', nargs=1) + parser.add_argument('--kid', nargs=1) + parser.add_argument('--kty', nargs=1) + + args = parser.parse_args() + + with open(args.config, 'r') as f: + config = json.load(f) + + keys = config["keys"] + + # Select a key, either explicitly or randomly + key_index = 0 + if args.key_index: + key_index = args.key_index[0] + print("args key_index " + str(key_index)) + else: + key_index = random.randint(0,len(keys)-1) + print("randomizing key index") + + print("Using key_index " + str(key_index)) + + print("Using Key: " + str(keys[key_index]["kid"]) + " to sign URI.") + key = keys[key_index] + + # Build Out claimset + claimset = {} + if "iss" in config.keys(): + claimset["iss"] = config["iss"] + + if "token_lifetime" in config.keys(): + claimset["exp"] = int(time.time()) + config["token_lifetime"] + else: + claimset["exp"] = int(time.time()) + 30 + + if "aud" in config.keys(): + claimset["aud"] = config["aud"] + + if "cdnistt" in config.keys(): + if config["cdnistt"]: + claimset["cdnistt"] = 1 + if "cdniets" in config.keys(): + claimset["cdniets"] = config["cdniets"] + else: + claimset["cdniets"] = 30 + + + # process override args - simple + if args.iss: + claimset["iss"] = args.iss[0] + if args.exp: + claimset["exp"] = args.exp[0] + if args.aud: + claimset["aud"] = args.aud[0] + + # process override args - complex + if args.cdnistt: + claimset["cdnistt"] = args.cdnistt[0] + + if "cdnistt" in config.keys(): + if args.cdniets: + claimset["cdniets"] = arg.cdniets[0] + + # specific key overrides + if args.alg: + key["alg"] = args.alg[0] + if args.kid: + key["kid"] = args.kid[0] + if args.kty: + key["kty"] = args.kty[0] + if args.k: + key["k"] = args.k[0] + + print(claimset) + print(key) + + Token = jwt.encode(claimset,key,algorithm=key["alg"]) + + print("Signed URL: " + args.uri + "?URISigningPackage=" + Token) + +if __name__ == "__main__": + main() diff --git a/plugins/experimental/uri_signing/timing.h b/plugins/experimental/uri_signing/timing.h index 9511bcdd5b3..20768a58bf9 100644 --- a/plugins/experimental/uri_signing/timing.h +++ b/plugins/experimental/uri_signing/timing.h @@ -16,6 +16,8 @@ * limitations under the License. */ +#pragma once + #include #include diff --git a/plugins/experimental/uri_signing/unit_tests/testConfig.config b/plugins/experimental/uri_signing/unit_tests/testConfig.config new file mode 100644 index 00000000000..aeecf363434 --- /dev/null +++ b/plugins/experimental/uri_signing/unit_tests/testConfig.config @@ -0,0 +1,102 @@ +{ + "Master Issuer": { + "renewal_kid": "6", + "id": "tester", + "auth_directives": [ + { + "auth": "allow", + "uri": "regex:invalid" + } + ], + "keys": [ + { + "alg": "HS256", + "k": "nxb7fyO5Z2hGz9E3oKm1357ptvC2su5QwQUb4YaIaIc", + "kid": "0", + "kty": "oct" + }, + { + "alg": "HS256", + "k": "cXKukBqFvQ0n3WAuRnWfExC14dmHdGoJULoZjGu9tJC", + "kid": "1", + "kty": "oct" + }, + { + "alg": "HS256", + "k": "38pJlSXfX87jWL0a03luml9QzUmM4qts1nmfIHA3B7r", + "kid": "2", + "kty": "oct" + }, + { + "alg": "HS256", + "k": "zNQPphknDGvzR5kA7IonXIDWKMyB1b8NpGmmDNlpgtM", + "kid": "3", + "kty": "oct" + }, + { + "alg": "HS256", + "k": "iB2ogCmQRt7r5hW7pgyP5FqiFcCl53MPQvfXv8wrZAn", + "kid": "4", + "kty": "oct" + }, + { + "alg": "HS256", + "k": "GJMCTyZhNoSOZvUOKmmY9MtGSLaONNLHqtKwsC3MWKo", + "kid": "5", + "kty": "oct" + }, + { + "alg": "HS256", + "k": "u2LziZKJFBnOfjUQUmvot7C9t91jj7ocJPIU9aDdbUl", + "kid": "6", + "kty": "oct" + }, + { + "alg": "HS256", + "k": "DRBKrBh87NYkH3UzfW1tWbiXCYXiYGZUE9w1orZngL0", + "kid": "7", + "kty": "oct" + }, + { + "alg": "HS256", + "k": "KNNKFbun8lEs7GbiKlo9mYGNdvpt33tdFzHbNnasDyP", + "kid": "8", + "kty": "oct" + }, + { + "alg": "HS256", + "k": "yb6kOddMUdupPRSkWMUdE6jrWT4MqUnVyTjpeJBYIqp", + "kid": "9", + "kty": "oct" + } + ] + }, + "Second Issuer": { + "keys": [ + { + "alg": "HS256", + "k": "testkey1", + "kid": "one", + "kty": "oct" + }, + { + "alg": "HS256", + "k": "testkey2", + "kid": "two", + "kty": "oct" + }, + { + "alg": "HS256", + "k": "testkey3", + "kid": "three", + "kty": "oct" + }, + { + "alg": "HS256", + "k": "testkey4", + "kid": "four", + "kty": "oct" + } + ] + } +} diff --git a/plugins/experimental/uri_signing/unit_tests/uri_signing_test.cc b/plugins/experimental/uri_signing/unit_tests/uri_signing_test.cc new file mode 100644 index 00000000000..20b2104eda0 --- /dev/null +++ b/plugins/experimental/uri_signing/unit_tests/uri_signing_test.cc @@ -0,0 +1,663 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +/* + * These are misc unit tests for uri signing + */ + +#define CATCH_CONFIG_MAIN +#include "catch.hpp" + +extern "C" { +#include +#include +#include "../jwt.h" +#include "../normalize.h" +#include "../parse.h" +#include "../match.h" +#include "../config.h" +} + +bool +jwt_parsing_helper(const char *jwt_string) +{ + fprintf(stderr, "Parsing JWT from string: %s\n", jwt_string); + bool resp; + json_error_t jerr = {}; + size_t pt_ct = strlen(jwt_string); + struct jwt *jwt = parse_jwt(json_loadb(jwt_string, pt_ct, 0, &jerr)); + + if (jwt) { + resp = jwt_validate(jwt); + } else { + resp = false; + } + + jwt_delete(jwt); + return resp; +} + +bool +normalize_uri_helper(const char *uri, const char *expected_normal) +{ + size_t uri_ct = strlen(uri); + int buff_size = uri_ct + 2; + int err; + char *uri_normal = static_cast(malloc(buff_size)); + memset(uri_normal, 0, buff_size); + + err = normalize_uri(uri, uri_ct, uri_normal, buff_size); + + if (err) { + free(uri_normal); + return false; + } + + if (expected_normal && strcmp(expected_normal, uri_normal) == 0) { + free(uri_normal); + return true; + } + + free(uri_normal); + return false; +} + +bool +remove_dot_helper(const char *path, const char *expected_path) +{ + fprintf(stderr, "Removing Dot Segments from Path: %s\n", path); + size_t path_ct = strlen(path); + path_ct++; + int new_ct; + char path_buffer[path_ct]; + memset(path_buffer, 0, path_ct); + + new_ct = remove_dot_segments(path, path_ct, path_buffer, path_ct); + + if (new_ct < 0) { + return false; + } else if (strcmp(expected_path, path_buffer) == 0) { + return true; + } else { + return false; + } +} + +bool +jws_parsing_helper(const char *uri, const char *paramName, const char *expected_strip) +{ + bool resp; + size_t uri_ct = strlen(uri); + size_t strip_ct = 0; + + char *uri_strip = static_cast(malloc(uri_ct + 1)); + memset(uri_strip, 0, uri_ct + 1); + + cjose_jws_t *jws = get_jws_from_uri(uri, uri_ct, paramName, uri_strip, uri_ct, &strip_ct); + if (jws) { + resp = true; + if (strcmp(uri_strip, expected_strip) != 0) { + cjose_jws_release(jws); + resp = false; + } + } else { + resp = false; + } + cjose_jws_release(jws); + free(uri_strip); + return resp; +} + +TEST_CASE("1", "[JWSParsingTest]") +{ + INFO("TEST 1, Test JWT Parsing From Token Strings"); + + SECTION("Standard JWT Parsing") + { + REQUIRE(jwt_parsing_helper("{\"cdniets\":30,\"cdnistt\":1,\"exp\":7284188499,\"iss\":\"Content Access " + "Manager\",\"cdniuc\":\"uri-regex:http://foobar.local/testDir/*\"}")); + } + + SECTION("JWT Parsing With Unknown Claim") + { + REQUIRE(jwt_parsing_helper("{\"cdniets\":30,\"cdnistt\":1,\"exp\":7284188499,\"iss\":\"Content Access " + "Manager\",\"cdniuc\":\"uri-regex:http://foobar.local/testDir/" + "*\",\"jamesBond\":\"Something,Something_else\"}")); + } + + SECTION("JWT Parsing with unsupported crit claim passed") + { + REQUIRE(!jwt_parsing_helper("{\"cdniets\":30,\"cdnistt\":1,\"exp\":7284188499,\"iss\":\"Content Access " + "Manager\",\"cdniuc\":\"uri-regex:http://foobar.local/testDir/" + "*\",\"cdnicrit\":\"Something,Something_else\"}")); + } + + SECTION("JWT Parsing with empty exp claim") + { + REQUIRE(jwt_parsing_helper("{\"cdniets\":30,\"cdnistt\":1,\"iss\":\"Content Access " + "Manager\",\"cdniuc\":\"uri-regex:http://foobar.local/testDir/*\"}")); + } + + SECTION("JWT Parsing with unsupported cdniip claim") + { + REQUIRE(!jwt_parsing_helper("{\"cdniets\":30,\"cdnistt\":1,\"cdniip\":\"123.123.123.123\",\"iss\":\"Content Access " + "Manager\",\"cdniuc\":\"uri-regex:http://foobar.local/testDir/*\"}")); + } + + SECTION("JWT Parsing with unsupported value for cdnistd claim") + { + REQUIRE(!jwt_parsing_helper("{\"cdniets\":30,\"cdnistt\":1,\"cdnistd\":4,\"iss\":\"Content Access " + "Manager\",\"cdniuc\":\"uri-regex:http://foobar.local/testDir/*\"}")); + } + fprintf(stderr, "\n"); +} + +TEST_CASE("2", "[JWSFromURLTest]") +{ + INFO("TEST 2, Test JWT Parsing and Stripping From URLs"); + + SECTION("Token at end of URI") + { + REQUIRE(jws_parsing_helper( + "www.foo.com/hellothere/" + "URISigningPackage=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", + "URISigningPackage", "www.foo.com/hellothere")); + } + + SECTION("No Token in URL") + { + REQUIRE(!jws_parsing_helper( + "www.foo.com/hellothere/" + "URISigningPackag=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", + "URISigningPackage", NULL)); + } + + SECTION("Token in middle of the URL") + { + REQUIRE(jws_parsing_helper("www.foo.com/hellothere/" + "URISigningPackage=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." + "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c/Something/Else", + "URISigningPackage", "www.foo.com/hellothere/Something/Else")); + } + + SECTION("Token at the start of the URL") + { + REQUIRE(jws_parsing_helper(":URISigningPackage=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." + "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c/www.foo.com/hellothere/Something/Else", + "URISigningPackage", "/www.foo.com/hellothere/Something/Else")); + } + + SECTION("Pass empty path parameter at end") + { + REQUIRE(!jws_parsing_helper("www.foobar.com/hellothere/URISigningPackage=", "URISigningPackage", NULL)); + } + + SECTION("Pass empty path parameter in the middle of URL") + { + REQUIRE(!jws_parsing_helper("www.foobar.com/hellothere/URISigningPackage=/Something/Else", "URISigningPackage", NULL)); + } + + SECTION("Partial package name in previous path parameter") + { + REQUIRE(jws_parsing_helper("www.foobar.com/URISig/" + "URISigningPackage=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." + "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c/Something/Else", + "URISigningPackage", "www.foobar.com/URISig/Something/Else")); + } + + SECTION("Package comes directly after two reserved characters") + { + REQUIRE(jws_parsing_helper("www.foobar.com/" + ":URISigningPackage=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." + "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c/Something/Else", + "URISigningPackage", "www.foobar.com//Something/Else")); + } + + SECTION("Package comes directly after string of reserved characters") + { + REQUIRE(jws_parsing_helper("www.foobar.com/?!/" + ":URISigningPackage=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." + "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c/Something/Else", + "URISigningPackage", "www.foobar.com/?!//Something/Else")); + } + + SECTION("Invalid token passed before a valid token") + { + REQUIRE(!jws_parsing_helper("www.foobar.com/URISigningPackage=/" + "URISigningPackage=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." + "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c/Something/Else", + "URISigningPackage", NULL)); + } + + SECTION("Empty string as URL") { REQUIRE(!jws_parsing_helper("", "URISigningPackage", NULL)); } + + SECTION("Empty package name to parser") + { + REQUIRE(!jws_parsing_helper( + "www.foobar.com/" + "URISigningPackage=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", + "", NULL)); + } + + SECTION("Custom package name with a reserved character - at the end of the URI") + { + REQUIRE(jws_parsing_helper( + "www.foobar.com/CustomPackage/" + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." + "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", + "CustomPackage/", "www.foobar.com")); + } + + SECTION("Custom package name with a reserved character - in the middle of the URI") + { + REQUIRE(jws_parsing_helper( + "www.foobar.com/CustomPackage/" + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." + "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c/Something/Else", + "CustomPackage/", "www.foobar.com/Something/Else")); + } + + SECTION("URI signing package passed as the only a query parameter") + { + REQUIRE(jws_parsing_helper( + "www.foobar.com/Something/" + "Here?URISigningPackage=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", + "URISigningPackage", "www.foobar.com/Something/Here")); + } + + SECTION("URI signing package passed as first of many query parameters") + { + REQUIRE(jws_parsing_helper("www.foobar.com/Something/" + "Here?URISigningPackage=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." + "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c&query3=foobar&query1=foo&query2=bar", + "URISigningPackage", "www.foobar.com/Something/Here?query3=foobar&query1=foo&query2=bar")); + } + + SECTION("URI signing package passed as one of many query parameters - passed in middle") + { + REQUIRE(jws_parsing_helper("www.foobar.com/Something/" + "Here?query1=foo&query2=bar&URISigningPackage=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." + "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c&query3=foobar", + "URISigningPackage", "www.foobar.com/Something/Here?query1=foo&query2=bar&query3=foobar")); + } + + SECTION("URI signing package passed as last of many query parameters") + { + REQUIRE(jws_parsing_helper("www.foobar.com/Something/" + "Here?query1=foo&query2=bar&URISigningPackage=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." + "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", + "URISigningPackage", "www.foobar.com/Something/Here?query1=foo&query2=bar")); + } +} + +TEST_CASE("3", "[RemoveDotSegmentsTest]") +{ + INFO("TEST 3, Test Removal of Dot Segments From Paths"); + + SECTION("../bar test") { REQUIRE(remove_dot_helper("../bar", "bar")); } + + SECTION("./bar test") { REQUIRE(remove_dot_helper("./bar", "bar")); } + + SECTION(".././bar test") { REQUIRE(remove_dot_helper(".././bar", "bar")); } + + SECTION("./../bar test") { REQUIRE(remove_dot_helper("./../bar", "bar")); } + + SECTION("/foo/./bar test") { REQUIRE(remove_dot_helper("/foo/./bar", "/foo/bar")); } + + SECTION("/bar/./ test") { REQUIRE(remove_dot_helper("/bar/./", "/bar/")); } + + SECTION("/. test") { REQUIRE(remove_dot_helper("/.", "/")); } + + SECTION("/bar/. test") { REQUIRE(remove_dot_helper("/bar/.", "/bar/")); } + + SECTION("/foo/../bar test") { REQUIRE(remove_dot_helper("/foo/../bar", "/bar")); } + + SECTION("/bar/../ test") { REQUIRE(remove_dot_helper("/bar/../", "/")); } + + SECTION("/.. test") { REQUIRE(remove_dot_helper("/..", "/")); } + + SECTION("/bar/.. test") { REQUIRE(remove_dot_helper("/bar/..", "/")); } + + SECTION("/foo/bar/.. test") { REQUIRE(remove_dot_helper("/foo/bar/..", "/foo/")); } + + SECTION("Single . test") { REQUIRE(remove_dot_helper(".", "")); } + + SECTION("Single .. test") { REQUIRE(remove_dot_helper("..", "")); } + + SECTION("Test foo/bar/.. test") { REQUIRE(remove_dot_helper("foo/bar/..", "foo/")); } + + SECTION("Test Empty Path Segment") { REQUIRE(remove_dot_helper("", "")); } + + SECTION("Test mixed operations") { REQUIRE(remove_dot_helper("/foo/bar/././something/../foobar", "/foo/bar/foobar")); } + fprintf(stderr, "\n"); +} + +TEST_CASE("4", "[NormalizeTest]") +{ + INFO("TEST 4, Test Normalization of URIs"); + + SECTION("Testing passing too small of a URI to normalize") { REQUIRE(!normalize_uri_helper("ht", NULL)); } + + SECTION("Testing passing non http/https protocol") { REQUIRE(!normalize_uri_helper("ht:", NULL)); } + + SECTION("Passing a uri with half encoded value at end") { REQUIRE(!normalize_uri_helper("http://www.foobar.co%4", NULL)); } + + SECTION("Passing a uri with half encoded value in the middle") + { + REQUIRE(!normalize_uri_helper("http://www.foobar.co%4psomethin/Path", NULL)); + } + + SECTION("Passing a uri with an empty path parameter") + { + REQUIRE(normalize_uri_helper("http://www.foobar.com", "http://www.foobar.com/")); + } + + SECTION("Passing a uri with an empty path parameter and additional query params") + { + REQUIRE(normalize_uri_helper("http://www.foobar.com?query1=foo&query2=bar", "http://www.foobar.com/?query1=foo&query2=bar")); + } + + SECTION("Empty path parameter with port") + { + REQUIRE(normalize_uri_helper("http://www.foobar.com:9301?query1=foo&query2=bar", + "http://www.foobar.com:9301/?query1=foo&query2=bar")); + } + + SECTION("Passing a uri with a username and password") + { + REQUIRE(normalize_uri_helper("http://foo%40:PaSsword@www.Foo%42ar.coM:80/", "http://foo%40:PaSsword@www.foobar.com/")); + } + + SECTION("Testing Removal of standard http Port") + { + REQUIRE(normalize_uri_helper("http://foobar.com:80/Something/Here", "http://foobar.com/Something/Here")); + } + + SECTION("Testing Removal of standard https Port") + { + REQUIRE(normalize_uri_helper("https://foobar.com:443/Something/Here", "https://foobar.com/Something/Here")); + } + + SECTION("Testing passing of non-standard http Port") + { + REQUIRE(normalize_uri_helper("http://foobar.com:443/Something/Here", "http://foobar.com:443/Something/Here")); + } + + SECTION("Testing passing of non-standard https Port") + { + REQUIRE(normalize_uri_helper("https://foobar.com:80/Something/Here", "https://foobar.com:80/Something/Here")); + } + + SECTION("Testing the removal of . and .. in the path ") + { + REQUIRE( + normalize_uri_helper("https://foobar.com:80/Something/Here/././foobar/../foo", "https://foobar.com:80/Something/Here/foo")); + } + + SECTION("Testing . and .. segments in non path components") + { + REQUIRE(normalize_uri_helper("https://foobar.com:80/Something/Here?query1=/././foo/../bar", + "https://foobar.com:80/Something/Here?query1=/././foo/../bar")); + } + + SECTION("Testing standard decdoing of multiple characters") + { + REQUIRE(normalize_uri_helper("https://kelloggs%54ester.com/%53omething/Here", "https://kelloggstester.com/Something/Here")); + } + + SECTION("Testing passing encoded reserved characters") + { + REQUIRE( + normalize_uri_helper("https://kelloggs%54ester.com/%53omething/Here%3f", "https://kelloggstester.com/Something/Here%3F")); + } + + SECTION("Mixed Bag Test case") + { + REQUIRE(normalize_uri_helper("https://foo:something@kellogs%54ester.com:443/%53omething/.././here", + "https://foo:something@kellogstester.com/here")); + } + + SECTION("Testing empty hostname with userinfon") { REQUIRE(!normalize_uri_helper("https://foo:something@", NULL)); } + + SECTION("Testing empty uri after http://") { REQUIRE(!normalize_uri_helper("http://", NULL)); } + + SECTION("Testing http:///////") { REQUIRE(!normalize_uri_helper("http:///////", NULL)); } + + SECTION("Testing empty uri after http://?/") { REQUIRE(!normalize_uri_helper("http://?/", NULL)); } + fprintf(stderr, "\n"); +} + +TEST_CASE("5", "[RegexTests]") +{ + INFO("TEST 5, Test Regex Matching"); + + SECTION("Standard regex") + { + REQUIRE(match_regex("http://kelloggsTester.souza.local/KellogsDir/*", + "http://kelloggsTester.souza.local/KellogsDir/some_manifest.m3u8")); + } + + SECTION("Back references are not supported") { REQUIRE(!match_regex("(b*a)\\1$", "bbbbba")); } + + SECTION("Escape a special character") { REQUIRE(match_regex("money\\$", "money$bags")); } + + SECTION("Dollar sign") + { + REQUIRE(!match_regex(".+foobar$", "foobarfoofoo")); + REQUIRE(match_regex(".+foobar$", "foofoofoobar")); + } + + SECTION("Number Quantifier with Groups") + { + REQUIRE(match_regex("(abab){2}", "abababab")); + REQUIRE(!match_regex("(abab){2}", "abab")); + } + + SECTION("Alternation") { REQUIRE(match_regex("cat|dog", "dog")); } + fprintf(stderr, "\n"); +} + +TEST_CASE("6", "[AudTests]") +{ + INFO("TEST 6, Test Aud Matching"); + + json_error_t *err = NULL; + SECTION("Standard aud string match") + { + json_t *raw = json_loads("{\"aud\": \"tester\"}", 0, err); + json_t *aud = json_object_get(raw, "aud"); + REQUIRE(jwt_check_aud(aud, "tester")); + json_decref(aud); + json_decref(raw); + } + + SECTION("Standard aud array match") + { + json_t *raw = json_loads("{\"aud\": [ \"foo\", \"bar\", \"tester\"]}", 0, err); + json_t *aud = json_object_get(raw, "aud"); + REQUIRE(jwt_check_aud(aud, "tester")); + json_decref(aud); + json_decref(raw); + } + + SECTION("Standard aud string mismatch") + { + json_t *raw = json_loads("{\"aud\": \"foo\"}", 0, err); + json_t *aud = json_object_get(raw, "aud"); + REQUIRE(!jwt_check_aud(aud, "tester")); + json_decref(aud); + json_decref(raw); + } + + SECTION("Standard aud array mismatch") + { + json_t *raw = json_loads("{\"aud\": [\"foo\", \"bar\", \"foobar\"]}", 0, err); + json_t *aud = json_object_get(raw, "aud"); + REQUIRE(!jwt_check_aud(aud, "tester")); + json_decref(aud); + json_decref(raw); + } + + SECTION("Integer trying to pass as an aud") + { + json_t *raw = json_loads("{\"aud\": 1}", 0, err); + json_t *aud = json_object_get(raw, "aud"); + REQUIRE(!jwt_check_aud(aud, "tester")); + json_decref(aud); + json_decref(raw); + } + + SECTION("Integer mixed into a passing aud array") + { + json_t *raw = json_loads("{\"aud\": [1, \"foo\", \"bar\", \"tester\"]}", 0, err); + json_t *aud = json_object_get(raw, "aud"); + REQUIRE(jwt_check_aud(aud, "tester")); + json_decref(aud); + json_decref(raw); + } + + SECTION("Case sensitive test for single string") + { + json_t *raw = json_loads("{\"aud\": \"TESTer\"}", 0, err); + json_t *aud = json_object_get(raw, "aud"); + REQUIRE(!jwt_check_aud(aud, "tester")); + json_decref(aud); + json_decref(raw); + } + + SECTION("Case sensitive test for array") + { + json_t *raw = json_loads("{\"aud\": [1, \"foo\", \"bar\", \"Tester\"]}", 0, err); + json_t *aud = json_object_get(raw, "aud"); + REQUIRE(!jwt_check_aud(aud, "tester")); + json_decref(aud); + json_decref(raw); + } + + fprintf(stderr, "\n"); +} + +TEST_CASE("7", "[TestsConfig]") +{ + INFO("TEST 7, Config Loading and Config Functions"); + + SECTION("Config Loading ID Field") + { + struct config *cfg = read_config("experimental/uri_signing/unit_tests/testConfig.config"); + REQUIRE(cfg != NULL); + REQUIRE(strcmp(config_get_id(cfg), "tester") == 0); + config_delete(cfg); + } + fprintf(stderr, "\n"); +} + +bool +jws_validation_helper(const char *url, const char *package, struct config *cfg) +{ + size_t url_ct = strlen(url); + size_t strip_ct = 0; + char uri_strip[url_ct + 1]; + memset(uri_strip, 0, sizeof uri_strip); + cjose_jws_t *jws = get_jws_from_uri(url, url_ct, package, uri_strip, url_ct, &strip_ct); + if (!jws) { + return false; + } + struct jwt *jwt = validate_jws(jws, cfg, uri_strip, strip_ct); + if (jwt) { + jwt_delete(jwt); + cjose_jws_release(jws); + return true; + } + cjose_jws_release(jws); + return false; +} + +TEST_CASE("8", "[TestsWithConfig]") +{ + INFO("TEST 8, Tests Involving Validation with Config"); + struct config *cfg = read_config("experimental/uri_signing/unit_tests/testConfig.config"); + + SECTION("Validation of Valid Aud String in JWS") + { + REQUIRE(jws_validation_helper("http://www.foobar.com/" + "URISigningPackage=eyJLZXlJREtleSI6IjUiLCJhbGciOiJIUzI1NiJ9." + "eyJjZG5pZXRzIjozMCwiY2RuaXN0dCI6MSwiaXNzIjoiTWFzdGVyIElzc3VlciIsImF1ZCI6InRlc3RlciIsImNkbml1YyI6" + "InJlZ2V4Omh0dHA6Ly93d3cuZm9vYmFyLmNvbS8qIn0.InBxVm6OOAglNqc-U5wAZaRQVebJ9PK7Y9i7VFHWYHU", + "URISigningPackage", cfg)); + fprintf(stderr, "\n"); + } + + SECTION("Validation of Invalid Aud String in JWS") + { + REQUIRE(!jws_validation_helper("http://www.foobar.com/" + "URISigningPackage=eyJLZXlJREtleSI6IjUiLCJhbGciOiJIUzI1NiJ9." + "eyJjZG5pZXRzIjozMCwiY2RuaXN0dCI6MSwiaXNzIjoiTWFzdGVyIElzc3VlciIsImF1ZCI6ImJhZCIsImNkbml1YyI6InJ" + "lZ2V4Omh0dHA6Ly93d3cuZm9vYmFyLmNvbS8qIn0.aCOo8gOBa5G1RKkkzgWYwc79dPRw_fQUC0k1sWcjkyM", + "URISigningPackage", cfg)); + fprintf(stderr, "\n"); + } + + SECTION("Validation of Valid Aud Array in JWS") + { + REQUIRE(jws_validation_helper( + "http://www.foobar.com/" + "URISigningPackage=eyJLZXlJREtleSI6IjUiLCJhbGciOiJIUzI1NiJ9." + "eyJjZG5pZXRzIjozMCwiY2RuaXN0dCI6MSwiaXNzIjoiTWFzdGVyIElzc3VlciIsImF1ZCI6WyJiYWQiLCJpbnZhbGlkIiwidGVzdGVyIl0sImNkbml1YyI6InJl" + "Z2V4Omh0dHA6Ly93d3cuZm9vYmFyLmNvbS8qIn0.7lyepZMzc_odieKvOTN2U-k1gLwRKS8KJIvDFQXDqGs", + "URISigningPackage", cfg)); + fprintf(stderr, "\n"); + } + + SECTION("Validation of Invalid Aud Array in JWS") + { + REQUIRE(!jws_validation_helper( + "http://www.foobar.com/" + "URISigningPackage=eyJLZXlJREtleSI6IjUiLCJhbGciOiJIUzI1NiJ9." + "eyJjZG5pZXRzIjozMCwiY2RuaXN0dCI6MSwiaXNzIjoiTWFzdGVyIElzc3VlciIsImF1ZCI6WyJiYWQiLCJpbnZhbGlkIiwiZm9vYmFyIl0sImNkbml1YyI6InJl" + "Z2V4Omh0dHA6Ly93d3cuZm9vYmFyLmNvbS8qIn0.CU3WMJAPs0uRC7NKXvatVG9uU9SANdZzqO0GdQUatxk", + "URISigningPackage", cfg)); + fprintf(stderr, "\n"); + } + + SECTION("Validation of Valid Aud Array Mixed types in JWS") + { + REQUIRE(jws_validation_helper( + "http://www.foobar.com/" + "URISigningPackage=eyJLZXlJREtleSI6IjUiLCJhbGciOiJIUzI1NiJ9." + "eyJjZG5pZXRzIjozMCwiY2RuaXN0dCI6MSwiaXNzIjoiTWFzdGVyIElzc3VlciIsImF1ZCI6WyJiYWQiLDEsImZvb2JhciIsInRlc3RlciJdLCJjZG5pdWMiOiJy" + "ZWdleDpodHRwOi8vd3d3LmZvb2Jhci5jb20vKiJ9._vlXsA3r7RPje2ZdMnpaGTwIsdNMjuQWPEHRkGKTVL8", + "URISigningPackage", cfg)); + fprintf(stderr, "\n"); + } + + config_delete(cfg); + fprintf(stderr, "\n"); +} diff --git a/plugins/experimental/uri_signing/uri_signing.c b/plugins/experimental/uri_signing/uri_signing.c index c648d183c76..fe6793bee84 100644 --- a/plugins/experimental/uri_signing/uri_signing.c +++ b/plugins/experimental/uri_signing/uri_signing.c @@ -16,16 +16,16 @@ * limitations under the License. */ -#include "uri_signing.h" +#include "common.h" #include "config.h" #include "parse.h" #include "jwt.h" #include "timing.h" -#include #include #include +#include #include #include @@ -46,7 +46,7 @@ TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size) return TS_ERROR; } - TSDebug(PLUGIN_NAME, "plugin is succesfully initialized"); + TSDebug(PLUGIN_NAME, "plugin is successfully initialized"); return TS_SUCCESS; } @@ -62,10 +62,23 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char *errbuf, int errbuf_s TSDebug(PLUGIN_NAME, "Initializing remap function of %s -> %s with config from %s", argv[0], argv[1], argv[2]); - const char *install_dir = TSInstallDirGet(); - size_t config_file_ct = snprintf(NULL, 0, "%s/%s/%s", install_dir, "etc/trafficserver", argv[2]); - char *config_file = malloc(config_file_ct + 1); - (void)snprintf(config_file, config_file_ct + 1, "%s/%s/%s", install_dir, "etc/trafficserver", argv[2]); + char const *const fname = argv[2]; + + if (0 == strlen(fname)) { + snprintf(errbuf, errbuf_size, "[TSRemapNewKeyInstance] - Invalid config file name for %s -> %s", argv[0], argv[1]); + return TS_ERROR; + } + + char *config_file = NULL; + if ('/' == fname[0]) { + config_file = strdup(fname); + } else { + char const *const config_dir = TSConfigDirGet(); + size_t const config_file_ct = snprintf(NULL, 0, "%s/%s", config_dir, fname); + config_file = malloc(config_file_ct + 1); + (void)snprintf(config_file, config_file_ct + 1, "%s/%s", config_dir, fname); + } + TSDebug(PLUGIN_NAME, "config file name: %s", config_file); struct config *cfg = read_config(config_file); if (!cfg) { @@ -157,8 +170,11 @@ TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri) int cpi = 0; int url_ct = 0; const char *url = NULL; + char *strip_uri = NULL; + TSRemapStatus status = TSREMAP_NO_REMAP; + bool checked_auth = false; - const char *package = "URISigningPackage"; + static char const *const package = "URISigningPackage"; TSMBuffer mbuf; TSMLoc ul; @@ -172,16 +188,23 @@ TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri) TSHandleMLocRelease(mbuf, TS_NULL_MLOC, ul); PluginDebug("Processing request for %.*s.", url_ct, url); - if (cpi < max_cpi) { - checkpoints[cpi++] = mark_timer(&t); - } - cjose_jws_t *jws = get_jws_from_query(url, url_ct, package); - if (cpi < max_cpi) { - checkpoints[cpi++] = mark_timer(&t); - } + checkpoints[cpi++] = mark_timer(&t); + + int strip_size = url_ct + 1; + strip_uri = (char *)TSmalloc(strip_size); + memset(strip_uri, 0, strip_size); + + size_t strip_ct; + cjose_jws_t *jws = get_jws_from_uri(url, url_ct, package, strip_uri, strip_size, &strip_ct); + + checkpoints[cpi++] = mark_timer(&t); + int checked_cookies = 0; if (!jws) { check_cookies: + /* There is no valid token in the url */ + strncpy(strip_uri, url, url_ct); + strip_ct = url_ct; ++checked_cookies; TSMLoc field; @@ -195,7 +218,11 @@ TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri) field = TSMimeHdrFieldFind(buffer, hdr, "Cookie", 6); if (field == TS_NULL_MLOC) { TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr); - goto fail; + if (!checked_auth) { + goto check_auth; + } else { + goto fail; + } } const char *client_cookie; @@ -206,7 +233,11 @@ TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri) TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr); if (!client_cookie || !client_cookie_ct) { - goto fail; + if (!checked_auth) { + goto check_auth; + } else { + goto fail; + } } size_t client_cookie_sz_ct = client_cookie_ct; check_more_cookies: @@ -214,7 +245,64 @@ TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri) checkpoints[cpi++] = mark_timer(&t); } jws = get_jws_from_cookie(&client_cookie, &client_cookie_sz_ct, package); + } else { + /* There has been a JWS found in the url */ + /* Strip the token from the URL for upstream if configured to do so */ + if (config_strip_token((struct config *)ih)) { + if ((int)strip_ct != url_ct) { + int map_url_ct = 0; + char *map_url = NULL; + char *map_strip_uri = NULL; + map_url = TSUrlStringGet(rri->requestBufp, rri->requestUrl, &map_url_ct); + + PluginDebug("Stripping Token from requestUrl: %s", map_url); + + int map_strip_size = map_url_ct + 1; + map_strip_uri = (char *)TSmalloc(map_strip_size); + memset(map_strip_uri, 0, map_strip_size); + size_t map_strip_ct = 0; + + cjose_jws_t *map_jws = get_jws_from_uri(map_url, map_url_ct, package, map_strip_uri, map_strip_size, &map_strip_ct); + cjose_jws_release(map_jws); + + char const *strip_uri_start = map_strip_uri; + + /* map_strip_uri is null terminated */ + size_t const mlen = strlen(strip_uri_start); + char const *strip_uri_end = strip_uri_start + mlen; + + PluginDebug("Stripping token from upstream url to: %.*s", (int)mlen, strip_uri_start); + + TSParseResult parse_rc = TSUrlParse(rri->requestBufp, rri->requestUrl, &strip_uri_start, strip_uri_end); + if (map_url != NULL) { + TSfree(map_url); + } + if (map_strip_uri != NULL) { + TSfree(map_strip_uri); + } + + if (parse_rc != TS_PARSE_DONE) { + PluginDebug("Error in TSUrlParse"); + goto fail; + } + status = TSREMAP_DID_REMAP; + } + } } +check_auth: + /* Check auth_dir and pass through if configured */ + if (uri_matches_auth_directive((struct config *)ih, url, url_ct)) { + PluginDebug("Auth directive matched for %.*s", url_ct, url); + if (url != NULL) { + TSfree((void *)url); + } + if (strip_uri != NULL) { + TSfree(strip_uri); + } + return TSREMAP_NO_REMAP; + } + checked_auth = true; + if (!jws) { goto fail; } @@ -222,8 +310,10 @@ TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri) if (cpi < max_cpi) { checkpoints[cpi++] = mark_timer(&t); } - struct jwt *jwt = validate_jws(jws, (struct config *)ih, url, url_ct); + + struct jwt *jwt = validate_jws(jws, (struct config *)ih, strip_uri, strip_ct); cjose_jws_release(jws); + if (cpi < max_cpi) { checkpoints[cpi++] = mark_timer(&t); } @@ -235,6 +325,8 @@ TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri) } } + /* There has been a validated JWT found in either the cookie or url */ + struct signer *signer = config_signer((struct config *)ih); char *cookie = renew(jwt, signer->issuer, signer->jwk, signer->alg, package); jwt_delete(jwt); @@ -256,16 +348,13 @@ TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri) last_mark = checkpoints[i]; } PluginDebug("Spent %" PRId64 " ns uri_signing verification of %.*s.", mark_timer(&t), url_ct, url); + TSfree((void *)url); - return TSREMAP_NO_REMAP; -fail: - if (uri_matches_auth_directive((struct config *)ih, url, url_ct)) { - if (url != NULL) { - TSfree((void *)url); - } - return TSREMAP_NO_REMAP; + if (strip_uri != NULL) { + TSfree(strip_uri); } - + return status; +fail: PluginDebug("Invalid JWT for %.*s", url_ct, url); TSHttpTxnStatusSet(txnp, TS_HTTP_STATUS_FORBIDDEN); PluginDebug("Spent %" PRId64 " ns uri_signing verification of %.*s.", mark_timer(&t), url_ct, url); @@ -273,6 +362,9 @@ TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri) if (url != NULL) { TSfree((void *)url); } + if (strip_uri != NULL) { + TSfree(strip_uri); + } return TSREMAP_DID_REMAP; } diff --git a/tests/Pipfile b/tests/Pipfile new file mode 100644 index 00000000000..b9a79bb2fb1 --- /dev/null +++ b/tests/Pipfile @@ -0,0 +1,40 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +autopep8 = "*" +pyflakes = "*" + +[packages] +autest = "==1.7.4" +traffic-replay = "*" # this should install TRLib, MicroServer, MicroDNS, Traffic-Replay +hyper = "*" +dnslib = "*" +# These are likely to be available via yum/dnf or apt-get +requests = "*" +gunicorn = "*" +httpbin = "*" +microserver = ">=1.0.4" +jsonschema = "*" +python-jose = "*" + +[requires] +python_version = "3" diff --git a/tests/gold_tests/pluginTest/uri_signing/config.json b/tests/gold_tests/pluginTest/uri_signing/config.json new file mode 100644 index 00000000000..e466cf740f5 --- /dev/null +++ b/tests/gold_tests/pluginTest/uri_signing/config.json @@ -0,0 +1,27 @@ +{ + "issuer": { + "id": "issuer", + "renewal_kid": "1", + "strip_token": true, + "auth_directives": [ + { + "auth": "allow", + "uri": "regex:.*crossdomain.xml" + } + ], + "keys": [ + { + "alg": "HS256", + "k": "SECRET00", + "kid": "0", + "kty": "oct" + }, + { + "alg": "HS256", + "k": "SECRET01", + "kid": "1", + "kty": "oct" + } + ] + } +} diff --git a/tests/gold_tests/pluginTest/uri_signing/gold/200.gold b/tests/gold_tests/pluginTest/uri_signing/gold/200.gold new file mode 100644 index 00000000000..afc41f9a171 --- /dev/null +++ b/tests/gold_tests/pluginTest/uri_signing/gold/200.gold @@ -0,0 +1,3 @@ +`` +< HTTP/1.1 200 OK +`` diff --git a/tests/gold_tests/pluginTest/uri_signing/gold/403.gold b/tests/gold_tests/pluginTest/uri_signing/gold/403.gold new file mode 100644 index 00000000000..1e2f71bb6f3 --- /dev/null +++ b/tests/gold_tests/pluginTest/uri_signing/gold/403.gold @@ -0,0 +1,3 @@ +`` +< HTTP/1.1 403 Forbidden +`` diff --git a/tests/gold_tests/pluginTest/uri_signing/run_sign.sh b/tests/gold_tests/pluginTest/uri_signing/run_sign.sh new file mode 100755 index 00000000000..375a83bfb7b --- /dev/null +++ b/tests/gold_tests/pluginTest/uri_signing/run_sign.sh @@ -0,0 +1,104 @@ +# Script to run sign.pl script. Single parameter is number 1 or greater selecting a set of script parameters. + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Preset arguments for generating tests +# +cmd_args () +{ +SELECT="$1" + +FUTURE="1923056084" # Monday, December 9, 2030 14:14:44 + +case "$SELECT" in +0) # future signing + echo "-c signer.json" + echo "-u http://somehost/someasset.ts" + echo "--exp=${FUTURE}" + echo "--key_index=0" + ;; +1) # expired signing (~1970) + echo "-c signer.json" + echo "-u http://somehost/someasset.ts" + echo "--key_index=0" + echo "--exp=1" + ;; +2) # future, second key + echo "-c signer.json" + echo "-u http://somehost/someasset.ts" + echo "--exp=${FUTURE}" + echo "--key_index=1" + ;; +3) + ;; +*) + echo "run_sign.sh: bad parameter" 1>&2 + exit 1 + ;; +esac +} + +# Find the path to the sign.pl script in the url_sig (source) directory. +# +find_cmd () +{ +local D T='..' +while [[ ! -d $T/.git ]] +do + if [[ ! -d $T/.. ]] ; then + echo "Working directory not in a git repo" 1>&2 + exit 1 + fi + T="$T/.." +done + +for D in $( find $T -name uri_signing -type d ) +do + if [[ -x $D/python_signer/uri_signer.py ]] ; then + echo "$D/python_signer/uri_signer.py" + return 0 + fi +done + +echo "cannot find uri_signer.py" 1>&2 +exit 1 +} + +# check for python-jose module +pip list | grep python-jose > /dev/null +status=$? +if [[ "$status" != 0 ]] ; then + echo "cannot find python-jose" 1>&2 + exit 1 +fi + +CMD=$( find_cmd ) +status=$? +if [[ "$status" != 0 ]] ; then + exit 1 +fi + + +ARGS=$( cmd_args "$1" ) +status=$? +if [[ "$status" != 0 ]] ; then + exit 1 +fi + +echo $CMD $ARGS + +$CMD $ARGS | tr ' ' '\n' | tail -1 diff --git a/tests/gold_tests/pluginTest/uri_signing/signer.json b/tests/gold_tests/pluginTest/uri_signing/signer.json new file mode 100644 index 00000000000..7c602f339ff --- /dev/null +++ b/tests/gold_tests/pluginTest/uri_signing/signer.json @@ -0,0 +1,18 @@ +{ + "iss": "issuer", + "token_lifetime": 90, + "keys": [ + { + "alg": "HS256", + "k": "SECRET00", + "kid": "0", + "kty": "oct" + }, + { + "alg": "HS256", + "k": "SECRET01", + "kid": "1", + "kty": "oct" + } + ] +} diff --git a/tests/gold_tests/pluginTest/uri_signing/uri_signing.test.py b/tests/gold_tests/pluginTest/uri_signing/uri_signing.test.py new file mode 100644 index 00000000000..0fb29e2d26e --- /dev/null +++ b/tests/gold_tests/pluginTest/uri_signing/uri_signing.test.py @@ -0,0 +1,212 @@ +''' +''' +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import subprocess +Test.Summary = ''' +Test uri_signing plugin +''' + +Test.ContinueOnFail = False + +# Skip if plugins not present. +Test.SkipUnless(Condition.PluginExists('uri_signing.so')) +Test.SkipUnless(Condition.PluginExists('cachekey.so')) + +server = Test.MakeOriginServer("server") + +# Default origin test +req_header = { "headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "", +} +res_header = { "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "", +} +server.addResponse("sessionfile.log", req_header, res_header) + +# Test case for normal +req_header = { "headers": + "GET /someasset.ts HTTP/1.1\r\nHost: somehost\r\n\r\n", + "timestamp": "1469733493.993", + "body": "", +} + +res_header = { "headers": + "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "somebody", +} + +server.addResponse("sessionfile.log", req_header, res_header) + +# Test case for crossdomain +req_header = { "headers": + "GET /crossdomain.xml HTTP/1.1\r\nHost: somehost\r\n\r\n", + "timestamp": "1469733493.993", + "body": "", +} + +res_header = { "headers": + "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "", +} + +server.addResponse("sessionfile.log", req_header, res_header) + +# http://user:password@host:port/path;params?query#fragment + +# Define default ATS +ts = Test.MakeATSProcess("ts") +#ts = Test.MakeATSProcess("ts", "traffic_server_valgrind.sh") + +ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'uri_signing|http', +# 'proxy.config.diags.debug.tags': 'uri_signing', + 'proxy.config.http.cache.http': 0, # No cache +}) + +# Use unchanged incoming URL. +# This uses cachekey to handle the effective vs pristine url diff for the +# first plugin issue that exists in 8.x. This is not necessary on 9x+ +ts.Disk.remap_config.AddLine( + 'map http://somehost/ http://127.0.0.1:{}/'.format(server.Variables.Port) + + ' @plugin=cachekey.so @pparam=--include-headers=foo' + + ' @plugin=uri_signing.so @pparam={}/config.json'.format(Test.RunDirectory) +) + +# Install configuration +ts.Setup.CopyAs('config.json', Test.RunDirectory) +ts.Setup.CopyAs('run_sign.sh', Test.RunDirectory) +ts.Setup.CopyAs('signer.json', Test.RunDirectory) +#ts.Setup.CopyAs('traffic_server_valgrind.sh', Test.RunDirectory) + +curl_and_args = 'curl -q -v -x localhost:{} '.format(ts.Variables.port) + +# 0 - reject unsigned request +tr = Test.AddTestRun("unsigned request") +ps = tr.Processes.Default +ps.StartBefore(ts) +ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) +ps.Command = curl_and_args + 'http://somehost/someasset.ts' +ps.ReturnCode = 0 +ps.Streams.stderr = "gold/403.gold" +tr.StillRunningAfter = server +tr.StillRunningAfter = ts + +# 1 - accept a passthru request +tr = Test.AddTestRun("passthru request") +ps = tr.Processes.Default +ps.Command = curl_and_args + 'http://somehost/crossdomain.xml' +ps.ReturnCode = 0 +ps.Streams.stderr = "gold/200.gold" +tr.StillRunningAfter = server +tr.StillRunningAfter = ts + +# 2 - good token, signed "forever" (run_sign.sh 0) +tr = Test.AddTestRun("good signed") +ps = tr.Processes.Default +ps.Command = curl_and_args + '"http://somehost/someasset.ts?URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE5MjMwNTYwODR9.zw_wFQ-wvrWmfPLGj3hAUWn-GOHkiJZi2but4KV0paY"' +ps.ReturnCode = 0 +ps.Streams.stderr = "gold/200.gold" +tr.StillRunningAfter = server +tr.StillRunningAfter = ts + +# 3 - expired token (run_sign.sh 1) +tr = Test.AddTestRun("expired signed") +ps = tr.Processes.Default +ps.Command = curl_and_args + '"http://somehost/someasset.ts?URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjF9.GkdlOPHQc6BqS4Q6x79GeYuVFO2zuGbaPZZsJfD6ir8"' +ps.ReturnCode = 0 +ps.Streams.stderr = "gold/403.gold" +tr.StillRunningAfter = server +tr.StillRunningAfter = ts + +# 4 - good token, different key (run_sign.sh 2) +tr = Test.AddTestRun("good token, second key") +ps = tr.Processes.Default +ps.Command = curl_and_args + '"http://somehost/someasset.ts?URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE5MjMwNTYwODR9.ozH4sNwgcOlTZT0l4RQlVCH_osxz9yI1HCBesEv-jYg"' +ps.ReturnCode = 0 +ps.Streams.stderr = "gold/200.gold" +tr.StillRunningAfter = server +tr.StillRunningAfter = ts + +# 5 - good token, inline +tr = Test.AddTestRun("good signed") +ps = tr.Processes.Default +ps.Command = curl_and_args + '"http://somehost/URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE5MjMwNTYwODR9.zw_wFQ-wvrWmfPLGj3hAUWn-GOHkiJZi2but4KV0paY/someasset.ts"' +ps.ReturnCode = 0 +ps.Streams.stderr = "gold/200.gold" +tr.StillRunningAfter = server +tr.StillRunningAfter = ts + +# 6 - expired token, inline +tr = Test.AddTestRun("expired signed") +ps = tr.Processes.Default +ps.Command = curl_and_args + '"http://somehost/URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjF9.GkdlOPHQc6BqS4Q6x79GeYuVFO2zuGbaPZZsJfD6ir8/someasset.ts"' +ps.ReturnCode = 0 +ps.Streams.stderr = "gold/403.gold" +tr.StillRunningAfter = server +tr.StillRunningAfter = ts + +# 7 - good token, param +tr = Test.AddTestRun("good signed, param") +ps = tr.Processes.Default +ps.Command = curl_and_args + '"http://somehost/someasset.ts;URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE5MjMwNTYwODR9.zw_wFQ-wvrWmfPLGj3hAUWn-GOHkiJZi2but4KV0paY"' +ps.ReturnCode = 0 +ps.Streams.stderr = "gold/200.gold" +tr.StillRunningAfter = server +tr.StillRunningAfter = ts + +# 8 - expired token, param +tr = Test.AddTestRun("expired signed, param") +ps = tr.Processes.Default +ps.Command = curl_and_args + '"http://somehost/someasset.ts;URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjF9.GkdlOPHQc6BqS4Q6x79GeYuVFO2zuGbaPZZsJfD6ir8"' +ps.ReturnCode = 0 +ps.Streams.stderr = "gold/403.gold" +tr.StillRunningAfter = server +tr.StillRunningAfter = ts + +# 9 - let's cookie this +tr = Test.AddTestRun("good signed cookie") +ps = tr.Processes.Default +ps.Command = curl_and_args + '"http://somehost/someasset.ts" -H "Cookie: URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE5MjMwNTYwODR9.zw_wFQ-wvrWmfPLGj3hAUWn-GOHkiJZi2but4KV0paY"' +ps.ReturnCode = 0 +ps.Streams.stderr = "gold/200.gold" +tr.StillRunningAfter = server +tr.StillRunningAfter = ts + +# 10 - expired cookie token +tr = Test.AddTestRun("expired signed cooked") +ps = tr.Processes.Default +ps.Command = curl_and_args + '"http://somehost/someasset.ts" -H "Cookie: URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjF9.GkdlOPHQc6BqS4Q6x79GeYuVFO2zuGbaPZZsJfD6ir8"' +ps.ReturnCode = 0 +ps.Streams.stderr = "gold/403.gold" +tr.StillRunningAfter = server +tr.StillRunningAfter = ts + +# 9 - multiple cookies +tr = Test.AddTestRun("multiple cookies, expired then good") +ps = tr.Processes.Default +ps.Command = curl_and_args + '"http://somehost/someasset.ts" -H "Cookie: URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjF9.GkdlOPHQc6BqS4Q6x79GeYuVFO2zuGbaPZZsJfD6ir8;URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE5MjMwNTYwODR9.zw_wFQ-wvrWmfPLGj3hAUWn-GOHkiJZi2but4KV0paY"' +ps.ReturnCode = 0 +ps.Streams.stderr = "gold/200.gold" +tr.StillRunningAfter = server +tr.StillRunningAfter = ts