diff --git a/doc/tang.8.adoc b/doc/tang.8.adoc
index e0c9f7d..f7ef06e 100644
--- a/doc/tang.8.adoc
+++ b/doc/tang.8.adoc
@@ -71,6 +71,27 @@ be changed with the *-p* option.
tang -l -p 9090
+== ENDPOINT
+
+The Tang server can be provided an endpoint. This endpoint will act as a prefix
+for the URL to be accessed by the client. This endpoint can be specified with
+the *-e* option.
+
+ tang -l -p 9090 -e this/is/an/endpoint
+
+When endpoint is specified, the endpoint will be prepended to the normal adv/rec
+URL. If no endpoint is provided, and assuming port 9090 is used, Tang server
+will listen on next URLs:
+
+ http://localhost:9090/adv (GET)
+ http://localhost:9090/rec (POST)
+
+If endpoint is provided, and assuming endpoint is /this/is/an/endpoint/, and
+assuming also port 9090 is used, Tang server will listen on next URLs:
+
+ http://localhost:9090/this/is/an/endpoint/adv (GET)
+ http://localhost:9090/this/is/an/endpoint/rec (POST)
+
== KEY ROTATION
In order to preserve the security of the system over the long run, you need to
diff --git a/src/socket.c b/src/socket.c
index bae7db9..ae3a065 100644
--- a/src/socket.c
+++ b/src/socket.c
@@ -146,8 +146,8 @@ static int listen_port(socket_list **slist, int port)
return r;
}
-static void spawn_process(int fd, const char *jwkdir,
- process_request_func pfunc,
+static void spawn_process(process_request_func pfunc,
+ process_request_func_args_t pfunc_args,
socket_list *slist)
{
pid_t pid;
@@ -159,21 +159,23 @@ static void spawn_process(int fd, const char *jwkdir,
close(ptr->s);
}
/* Ensure that both stdout and stdin are set */
- if (dup2(fd, STDOUT_FILENO) < 0) {
+ if (dup2(pfunc_args.fileno, STDOUT_FILENO) < 0) {
perror("dup2");
- close(fd);
+ close(pfunc_args.fileno);
return;
}
- close(fd);
+ close(pfunc_args.fileno);
- pfunc(jwkdir, STDOUT_FILENO);
+ process_request_func_args_t pf =
+ { pfunc_args.jwkdir, STDOUT_FILENO, pfunc_args.endpoint };
+ pfunc(pf);
free_socket_list(slist);
exit(0);
} else if (pid == -1) {
perror("fork failed");
}
- close(fd);
+ close(pfunc_args.fileno);
}
static void handle_child(int sig)
@@ -184,7 +186,7 @@ static void handle_child(int sig)
while ((pid = waitpid(-1, &status, WNOHANG)) > 0);
}
-int run_service(const char *jwkdir, int port, process_request_func pfunc)
+int run_service(run_service_args_t rserv, process_request_func pfunc)
{
socket_list *slist, *ptr;
int r, n = 0, accept_fd;
@@ -199,9 +201,9 @@ int run_service(const char *jwkdir, int port, process_request_func pfunc)
new_action.sa_flags = 0;
sigaction(SIGCHLD, &new_action, NULL);
- r = listen_port(&slist, port);
+ r = listen_port(&slist, rserv.port);
if (r < 0) {
- fprintf(stderr, "Could not listen port (%d)\n", port);
+ fprintf(stderr, "Could not listen port (%d)\n", rserv.port);
return -1;
}
@@ -234,8 +236,9 @@ int run_service(const char *jwkdir, int port, process_request_func pfunc)
perror("accept");
continue;
}
-
- spawn_process(accept_fd, jwkdir, pfunc, slist);
+ process_request_func_args_t pr_args =
+ {rserv.jwkdir, accept_fd,rserv.endpoint};
+ spawn_process(pfunc, pr_args, slist);
}
}
diff --git a/src/socket.h b/src/socket.h
index 7cfd873..7392d55 100644
--- a/src/socket.h
+++ b/src/socket.h
@@ -15,7 +15,18 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
+typedef struct process_request_func_args {
+ const char* jwkdir;
+ const int fileno;
+ const char* endpoint;
+} process_request_func_args_t;
-typedef int (*process_request_func)(const char *jwkdir, int in_fileno);
+typedef int (*process_request_func)(const process_request_func_args_t);
-int run_service(const char *jwkdir, int port, process_request_func);
+typedef struct run_service_args {
+ const char* jwkdir;
+ const int port;
+ const char* endpoint;
+} run_service_args_t;
+
+int run_service(const run_service_args_t, process_request_func);
diff --git a/src/tang-show-keys b/src/tang-show-keys
index 0c33c3a..3ba82e7 100755
--- a/src/tang-show-keys
+++ b/src/tang-show-keys
@@ -20,14 +20,23 @@
set -e
-if [ $# -gt 1 ]; then
- echo "Usage: $0 []" >&2
+if [ $# -gt 2 ]; then
+ echo "Usage: $0 [] []" >&2
exit 1
fi
port=${1-80}
-adv=$(curl -sSf "localhost:$port/adv")
+if test -n "$2"; then
+ first_letter=$(printf %.1s "$2")
+ if [ "${first_letter}" = "/" ]; then
+ adv=$(curl -sSf "localhost:$port$2/adv")
+ else
+ adv=$(curl -sSf "localhost:$port/$2/adv")
+ fi
+else
+ adv=$(curl -sSf "localhost:$port/adv")
+fi
THP_DEFAULT_HASH=S256 # SHA-256.
jose fmt --json "${adv}" -g payload -y -o- \
diff --git a/src/tangd.c b/src/tangd.c
index 1e3a6a3..e12b54b 100644
--- a/src/tangd.c
+++ b/src/tangd.c
@@ -32,8 +32,11 @@
#include "keys.h"
#include "socket.h"
+const unsigned int MAX_URL = 256;
+
static const struct option long_options[] = {
{"port", 1, 0, 'p'},
+ {"endpoint", 1, 0, 'e'},
{"listen", 0, 0, 'l'},
{"version", 0, 0, 'v'},
{"help", 0, 0, 'h'},
@@ -45,6 +48,7 @@ print_help(const char *name)
{
fprintf(stderr, "Usage: %s [OPTIONS] \n", name);
fprintf(stderr, " -p, --port=PORT Specify the port to listen (default 9090)\n");
+ fprintf(stderr, " -e, --endpoint=ENDPOINT Specify endpoint to listen (empty by default)\n");
fprintf(stderr, " -l, --listen Run as a service and wait for connections\n");
fprintf(stderr, " -v, --version Display program version\n");
fprintf(stderr, " -h, --help Show this help message\n");
@@ -184,19 +188,37 @@ rec(enum http_method method, const char *path, const char *body,
"\r\n%s", strlen(enc), enc);
}
-static struct http_dispatch dispatch[] = {
- { adv, 1 << HTTP_GET, 2, "^/+adv/+([0-9A-Za-z_-]+)$" },
- { adv, 1 << HTTP_GET, 2, "^/+adv/*$" },
- { rec, 1 << HTTP_POST, 2, "^/+rec/+([0-9A-Za-z_-]+)$" },
- {}
-};
-
#define DEFAULT_PORT 9090
static int
-process_request(const char *jwkdir, int in_fileno)
+process_request(const process_request_func_args_t pr_args)
{
- struct http_state state = { .dispatch = dispatch, .misc = (char*)jwkdir };
+ char adv_endpoint[MAX_URL];
+ char adv2_endpoint[MAX_URL];
+ char rec_endpoint[MAX_URL];
+ if (pr_args.endpoint != NULL) {
+ if (pr_args.endpoint[0] == '/') {
+ snprintf(adv_endpoint, MAX_URL, "^%s/+adv/+([0-9A-Za-z_-]+)$", pr_args.endpoint);
+ snprintf(adv2_endpoint, MAX_URL, "^%s/+adv/*$", pr_args.endpoint);
+ snprintf(rec_endpoint, MAX_URL, "^%s/+rec/+([0-9A-Za-z_-]+)$", pr_args.endpoint);
+ } else {
+ snprintf(adv_endpoint, MAX_URL, "^/%s/+adv/+([0-9A-Za-z_-]+)$", pr_args.endpoint);
+ snprintf(adv2_endpoint, MAX_URL, "^/%s/+adv/*$", pr_args.endpoint);
+ snprintf(rec_endpoint, MAX_URL, "^/%s/+rec/+([0-9A-Za-z_-]+)$", pr_args.endpoint);
+ }
+ } else {
+ strncpy(adv_endpoint, "^/+adv/+([0-9A-Za-z_-]+)$", MAX_URL);
+ strncpy(adv2_endpoint, "^/+adv/*$", MAX_URL);
+ strncpy(rec_endpoint, "^/+rec/+([0-9A-Za-z_-]+)$", MAX_URL);
+ }
+ struct http_dispatch dispatch[] = {
+ { adv, 1 << HTTP_GET, 2, adv_endpoint },
+ { adv, 1 << HTTP_GET, 2, adv2_endpoint },
+ { rec, 1 << HTTP_POST, 2, rec_endpoint },
+ {}
+ };
+
+ struct http_state state = { .dispatch = dispatch, .misc = (char*)pr_args.jwkdir };
struct http_parser parser = { .data = &state };
struct stat st = {};
char req[4096] = {};
@@ -205,18 +227,18 @@ process_request(const char *jwkdir, int in_fileno)
http_parser_init(&parser, HTTP_REQUEST);
- if (stat(jwkdir, &st) != 0) {
- fprintf(stderr, "Error calling stat() on path: %s: %m\n", jwkdir);
+ if (stat(pr_args.jwkdir, &st) != 0) {
+ fprintf(stderr, "Error calling stat() on path: %s: %m\n", pr_args.jwkdir);
return EXIT_FAILURE;
}
if (!S_ISDIR(st.st_mode)) {
- fprintf(stderr, "Path is not a directory: %s\n", jwkdir);
+ fprintf(stderr, "Path is not a directory: %s\n", pr_args.jwkdir);
return EXIT_FAILURE;
}
for (;;) {
- r = read(in_fileno, &req[rcvd], sizeof(req) - rcvd - 1);
+ r = read(pr_args.fileno, &req[rcvd], sizeof(req) - rcvd - 1);
if (r == 0)
return rcvd > 0 ? EXIT_FAILURE : EXIT_SUCCESS;
if (r < 0)
@@ -244,9 +266,10 @@ main(int argc, char *argv[])
int listen = 0;
int port = DEFAULT_PORT;
const char *jwkdir = NULL;
+ const char *endpoint = NULL;
while (1) {
- int c = getopt_long(argc, argv, "lp:vh", long_options, NULL);
+ int c = getopt_long(argc, argv, "lp:e:vh", long_options, NULL);
if (c == -1)
break;
@@ -260,6 +283,9 @@ main(int argc, char *argv[])
case 'p':
port = atoi(optarg);
break;
+ case 'e':
+ endpoint = optarg;
+ break;
case 'l':
listen = 1;
break;
@@ -273,8 +299,10 @@ main(int argc, char *argv[])
jwkdir = argv[optind++];
if (listen == 0) { /* process one-shot query from stdin */
- return process_request(jwkdir, STDIN_FILENO);
+ process_request_func_args_t pr = {jwkdir, STDIN_FILENO, endpoint};
+ return process_request(pr);
} else { /* listen and process all incoming connections */
- return run_service(jwkdir, port, process_request);
+ run_service_args_t rs = {jwkdir, port, endpoint};
+ return run_service(rs, process_request);
}
}
diff --git a/tests/adv b/tests/adv
index 8e6dd0e..416dc6e 100755
--- a/tests/adv
+++ b/tests/adv
@@ -36,47 +36,47 @@ adv_startup () {
adv_second_phase () {
# Make sure requests on the root fail
- fetch / && expected_fail
+ fetch "${ENDPOINT}"/ && expected_fail
# The request should fail (404) for non-signature key IDs
- fetch /adv/`jose jwk thp -i $TMP/db/exc.jwk` && expected_fail
- fetch /adv/`jose jwk thp -a S512 -i $TMP/db/exc.jwk` && expected_fail
+ fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/exc.jwk` && expected_fail
+ fetch "${ENDPOINT}"/adv/`jose jwk thp -a S512 -i $TMP/db/exc.jwk` && expected_fail
# The default advertisement fetch should succeed and pass verification
- fetch /adv
- fetch /adv | ver $TMP/db/sig.jwk
- fetch /adv/ | ver $TMP/db/sig.jwk
+ fetch "${ENDPOINT}"/adv
+ fetch "${ENDPOINT}"/adv | ver $TMP/db/sig.jwk
+ fetch "${ENDPOINT}"/adv/ | ver $TMP/db/sig.jwk
# Fetching by any thumbprint should work
- fetch /adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/sig.jwk
- fetch /adv/`jose jwk thp -a S512 -i $TMP/db/sig.jwk` | ver $TMP/db/sig.jwk
+ fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/sig.jwk
+ fetch "${ENDPOINT}"/adv/`jose jwk thp -a S512 -i $TMP/db/sig.jwk` | ver $TMP/db/sig.jwk
# Requesting an adv by an advertised key ID should't be signed by hidden keys
- fetch /adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/.sig.jwk && expected_fail
- fetch /adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/.oth.jwk && expected_fail
+ fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/.sig.jwk && expected_fail
+ fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/.oth.jwk && expected_fail
# Verify that the default advertisement is not signed with hidden signature keys
- fetch /adv/ | ver $TMP/db/.oth.jwk && expected_fail
- fetch /adv/ | ver $TMP/db/.sig.jwk && expected_fail
+ fetch "${ENDPOINT}"/adv/ | ver $TMP/db/.oth.jwk && expected_fail
+ fetch "${ENDPOINT}"/adv/ | ver $TMP/db/.sig.jwk && expected_fail
# A private key advertisement is signed by all advertised keys and the requested private key
- fetch /adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/sig.jwk
- fetch /adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/.sig.jwk
- fetch /adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/.oth.jwk && expected_fail
+ fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/sig.jwk
+ fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/.sig.jwk
+ fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/.oth.jwk && expected_fail
# Verify that the advertisements contain the cty parameter
- fetch /adv | jose fmt -j- -Og protected -SyOg cty -Sq "jwk-set+json" -E
- fetch /adv/`jose jwk thp -i $TMP/db/.sig.jwk` \
+ fetch "${ENDPOINT}"/adv | jose fmt -j- -Og protected -SyOg cty -Sq "jwk-set+json" -E
+ fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/.sig.jwk` \
| jose fmt -j- -Og signatures -A \
-g 0 -Og protected -SyOg cty -Sq "jwk-set+json" -EUUUUU \
-g 1 -Og protected -SyOg cty -Sq "jwk-set+json" -EUUUUU
THP_DEFAULT_HASH=S256 # SHA-256.
- test "$(tang-show-keys $PORT)" = "$(jose jwk thp -a "${THP_DEFAULT_HASH}" -i $TMP/db/sig.jwk)"
+ test "$(tang-show-keys $PORT $ENDPOINT)" = "$(jose jwk thp -a "${THP_DEFAULT_HASH}" -i $TMP/db/sig.jwk)"
# Check that new keys will be created if none exist.
rm -rf "${TMP}/db" && mkdir -p "${TMP}/db"
- fetch /adv
+ fetch "${ENDPOINT}"/adv
# Now let's make sure the new keys were named using our default thumbprint
# hash and then rotate them and check if we still create new keys.
@@ -88,7 +88,7 @@ adv_second_phase () {
mv -f -- "${k}" ".${k}"
done
cd -
- fetch /adv
+ fetch "${ENDPOINT}"/adv
# Lets's now test with multiple pairs of keys.
for i in 1 2 3 4 5 6 7 8 9; do
@@ -103,12 +103,12 @@ adv_second_phase () {
done
# Verify the advertisement is correct.
- validate "$(fetch /adv)"
+ validate "$(fetch "${ENDPOINT}"/adv)"
# And make sure we can fetch an adv by its thumbprint.
for jwk in "${TMP}"/db/other-sig-*.jwk; do
for alg in $(jose alg -k hash); do
- fetch /adv/"$(jose jwk thp -a "${alg}" -i "${jwk}")" | ver "${jwk}"
+ fetch "${ENDPOINT}"/adv/"$(jose jwk thp -a "${alg}" -i "${jwk}")" | ver "${jwk}"
done
done
@@ -130,5 +130,5 @@ adv_second_phase () {
valid_key_perm "${jwk}"
done
[ -z "${thp}" ] && die "There should be valid keys after rotation"
- test "$(tang-show-keys $PORT)" = "${thp}"
+ test "$(tang-show-keys $PORT $ENDPOINT)" = "${thp}"
}
diff --git a/tests/adv-socat-endpoint b/tests/adv-socat-endpoint
new file mode 100644
index 0000000..c4deb88
--- /dev/null
+++ b/tests/adv-socat-endpoint
@@ -0,0 +1,33 @@
+#!/bin/sh -ex
+#
+# Copyright (c) 2023 Red Hat, Inc.
+# Author: Sergio Arroutbi
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+
+. adv
+
+sanity_check
+
+adv_startup
+
+port=$(random_port)
+export PORT=$((port+3))
+export ENDPOINT="/api/dee-hms"
+start_server_endpoint "${PORT}" "${ENDPOINT}"
+export PID=$!
+wait_for_port ${PORT}
+
+adv_second_phase
diff --git a/tests/adv-standalone-endpoint b/tests/adv-standalone-endpoint
new file mode 100644
index 0000000..de7a7fe
--- /dev/null
+++ b/tests/adv-standalone-endpoint
@@ -0,0 +1,31 @@
+#!/bin/sh -ex
+#
+# Copyright (c) 2023 Red Hat, Inc.
+# Author: Sergio Arroutbi
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+
+. adv
+
+adv_startup
+
+port=$(random_port)
+export PORT=$((port+1))
+export ENDPOINT="/api/dee-hms"
+start_standalone_server_endpoint "${PORT}" "${ENDPOINT}"
+export PID=$!
+wait_for_port ${PORT}
+
+adv_second_phase
diff --git a/tests/helpers b/tests/helpers
index 55447ae..5ce3daf 100755
--- a/tests/helpers
+++ b/tests/helpers
@@ -30,7 +30,12 @@ random_port() {
if [ -n "${TANG_BSD}" ]; then
jot -r 1 1024 65536
else
- shuf -i 1024-65536 -n 1
+ if test -f /dev/urandom;
+ then
+ shuf -i 1024-65535 -n 1 --random-file=/dev/urandom
+ else
+ shuf -i 1024-65535 -n 1
+ fi
fi
}
@@ -62,10 +67,18 @@ start_server() {
"${SOCAT}" TCP-LISTEN:"${1}",bind=127.0.0.1,fork SYSTEM:"${VALGRIND} tangd ${TMP}/db" &
}
+start_server_endpoint() {
+ "${SOCAT}" TCP-LISTEN:"${1}",bind=127.0.0.1,fork SYSTEM:"${VALGRIND} tangd ${TMP}/db -e ${ENDPOINT}" &
+}
+
start_standalone_server() {
${VALGRIND} tangd -p ${1} -l ${TMP}/db &
}
+start_standalone_server_endpoint() {
+ ${VALGRIND} tangd -p ${1} -l ${TMP}/db -e ${2} &
+}
+
on_exit() {
if [ "${PID}" ]; then
kill "${PID}" || true
diff --git a/tests/meson.build b/tests/meson.build
index f869baf..4b3a02d 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -41,9 +41,13 @@ if socat.found()
endif
test('adv-standalone', find_program('adv-standalone'), env: env, timeout: 60)
+test('adv-standalone-endpoint', find_program('adv-standalone-endpoint'), env: env, timeout: 60)
test('adv-socat', find_program('adv-socat'), env: env, timeout: 60)
+test('adv-socat-endpoint', find_program('adv-socat-endpoint'), env: env, timeout: 60)
test('rec-standalone', find_program('rec-standalone'), env: env, timeout: 60)
+test('rec-standalone-endpoint', find_program('rec-standalone-endpoint'), env: env, timeout: 60)
test('rec-socat', find_program('rec-socat'), env: env)
+test('rec-socat-endpoint', find_program('rec-socat-endpoint'), env: env)
test('test-keys', test_keys, env: env, timeout: 60)
# vim:set ts=2 sw=2 et:
diff --git a/tests/rec b/tests/rec
index f15637d..d30f72f 100755
--- a/tests/rec
+++ b/tests/rec
@@ -51,3 +51,17 @@ rec_second_phase () {
http://127.0.0.1:$PORT/rec/${exc_kid} < $TMP/exc.pub.jwk`
[ "$good" = "$test" ]
}
+
+rec_second_phase_endpoint () {
+ # Make sure that GET fails
+ curl -sf http://127.0.0.1:$PORT/$ENDPOINT/rec && expected_fail
+ curl -sf http://127.0.0.1:$PORT/$ENDPOINT/rec/ && expected_fail
+
+ # Make a recovery request (NOTE: this is insecure! Don't do this in real code!)
+ good=`jose jwk exc -i '{"alg":"ECMR","key_ops":["deriveKey"]}' -l $TMP/exc.jwk -r $TMP/db/exc.jwk`
+ test=`curl -sf -X POST \
+ -H "Content-Type: application/jwk+json" \
+ --data-binary @- \
+ http://127.0.0.1:$PORT/$ENDPOINT/rec/${exc_kid} < $TMP/exc.pub.jwk`
+ [ "$good" = "$test" ]
+}
diff --git a/tests/rec-socat-endpoint b/tests/rec-socat-endpoint
new file mode 100644
index 0000000..f62d919
--- /dev/null
+++ b/tests/rec-socat-endpoint
@@ -0,0 +1,34 @@
+#!/bin/sh -ex
+#
+# Copyright (c) 2023 Red Hat, Inc.
+# Author: Sergio Arroutbi
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+
+. rec
+
+sanity_check
+
+rec_startup
+
+# Start the server
+port=$(random_port)
+export PORT=$((port+4))
+export ENDPOINT="api/dee-hms"
+start_server_endpoint "${PORT}" "${ENDPOINT}"
+export PID=$!
+wait_for_port ${PORT}
+
+rec_second_phase_endpoint
diff --git a/tests/rec-standalone-endpoint b/tests/rec-standalone-endpoint
new file mode 100755
index 0000000..2e7e1b8
--- /dev/null
+++ b/tests/rec-standalone-endpoint
@@ -0,0 +1,34 @@
+#!/bin/sh -ex
+#
+# Copyright (c) 2023 Red Hat, Inc.
+# Author: Sergio Arroutbi
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+
+. rec
+
+sanity_check
+
+rec_startup
+
+# Start the server
+port=$(random_port)
+export PORT=$((port+2))
+export ENDPOINT="api/dee-hms"
+start_standalone_server_endpoint "${PORT}" "${ENDPOINT}"
+export PID=$!
+wait_for_port ${PORT}
+
+rec_second_phase_endpoint