aboutsummaryrefslogtreecommitdiff
path: root/cloyster
diff options
context:
space:
mode:
authorJordan Bracco <href@random.sh>2022-03-22 21:36:58 +0000
committerJordan Bracco <href@random.sh>2022-03-22 21:36:58 +0000
commit435531e42f3d1b1d0de291071aedb111d727e57d (patch)
treef51ae7f59750b41f6ac416e01f102801a1c420b6 /cloyster
Initial PoC
Diffstat (limited to 'cloyster')
-rwxr-xr-xcloyster276
1 files changed, 276 insertions, 0 deletions
diff --git a/cloyster b/cloyster
new file mode 100755
index 0000000..18f72ca
--- /dev/null
+++ b/cloyster
@@ -0,0 +1,276 @@
+#!/bin/sh
+XTRACE="${XTRACE:-}"
+XTRACE_EXEC="${EXEC_XTRACE:-}"
+DEBUG="${DEBUG:-}"
+hostname="${HOSTNAME:-$(hostname)}"
+_executing=
+_exec_error=
+_script_name=$(basename "$0" | cut -d '.' -f 1)
+
+# is_true VARIABLE
+# return 0 if VARIABLE is "y", "yes", "t", "true", "1". Otherwise returns 1.
+# Example: `if _is_true "${XTRACE}"; then set -x; fi`
+is_true() {
+ _t=1
+ case "$1" in
+ y) _t=0;; yes) _t=0;;
+ t) _t=0;; true) _t=0;;
+ 1) _t=0;;
+ *) _t=1;;
+ esac
+ return "${_t}"
+}
+
+if is_true "${XTRACE}"; then set -x; fi
+
+# Exit early if we don't have `jo`.
+if ! which jo > /dev/null 2>&1; then
+ >&2 echo '{"error":{"type":"execution_error","error":"missing_dependency","dependency":"jo"}}'
+ exit 1
+fi
+
+_debug() {
+ _line=$1
+ shift
+ if is_true "${DEBUG}"; then
+ if is_true "${_executing}"; then
+ _date=$(date_now_iso_utc)
+ echo "${_line}" | jo "output=cloyster" "date=${_date}" "line=@-" "${@}" >> "${_cloyster_dest}"
+ else
+ >&2 echo "[${_script_name}:debug] ${_line} $@"
+ fi
+ fi
+}
+
+_execution_error() {
+ _debug "fun=_execution_error() executing=${_executing}"
+ >&2 jo error="$(jo type=execution "$@")"
+}
+
+. ./lib/dependencies.sh
+ensure_command_exists jq
+ensure_command_exists bc
+
+. ./lib/os.sh
+_debug "os=$(os_get jq '. | @json')"
+if ! is_true $(os_get "supported"); then
+ _execution_error error=unsupported_operating_system
+fi
+
+. ./lib/identifiers.sh
+. ./lib/datetime.sh
+
+## getopts
+input_decode=
+output_encode=
+stdin=
+while getopts :Bbs _opt; do
+ case "${_opt}" in
+ b) input_decode="base64";;
+ B) output_encode="base64";;
+ s) stdin=yes;;
+ *) _execution_error error=invalid_option option="${_opt}";;
+ esac
+done
+shift $((OPTIND - 1))
+
+if is_true "${stdin}" || [ "${1}" = "-" ]; then
+ if [ "${1}" = "-" ]; then shift; fi
+ stdin=yes
+ _exec="STDIN"
+else
+ _exec="${1}"
+ shift
+ if [ -z "${_exec}" ]; then
+ _execution_error error=no_script_specified
+ elif [ ! -f "${_exec}" ]; then
+ _execution_error error=missing_script script="${_exec}"
+ elif ! head -n 1 "${_exec}" | grep "/cloyster" > /dev/null 2>&1; then
+ _execution_error error=missing_shebang script="${_exec}"
+ fi
+fi
+
+_args_json=
+if [ ! "${#}" = "0" ]; then
+ _args_json="$(jo -d. $@)"
+ _debug "args_json=$_args_json"
+fi
+
+args_get() {
+ _jqq="${1}"
+ if [ -z "${_jqq}" ]; then
+ _jqq="."
+ fi
+ value="$(echo ${_args_json} | jq -r "${_jqq}")"
+ if [ "${value}" = "null" ]; then value=; fi
+ echo "${value}"
+}
+
+json_quote() {
+ echo "$1" | jq '. | @json'
+}
+json_unquote() {
+ echo "$1" | jq -r '.'
+}
+
+_trap_exit() {
+ _debug "TRAPPED EXIT >:( $@ executing? ${_executing}"
+ if is_true "${_executing}"; then _exec_post "TRAPEXIT"; fi
+ _debug "TRAPPED EXIT >:( $@ executing? ${_executing}"
+ #rm "${_tmp_base}"* > /dev/null 2>&1 || true
+ ( _json_loggers_stop ) > /dev/null 2>&1
+ _debug "$(jobs)"
+ _debug "bye"
+ if is_true "${_executing}"; then exit 1; else exit 0; fi
+}
+#trap _trap_exit EXIT
+
+_exec_name=$(basename "$_exec")
+_tmp_base="/tmp/${_script_name}@${_exec_name}@$(make_random_identifier 16)"
+_stdout_dest="${_tmp_base}_stdout.log"
+_stderr_dest="${_tmp_base}_stderr.log"
+_cloyster_dest="${_tmp_base}_cloyster.jsonstream"
+_output_dest="${_tmp_base}_output.jsonstream"
+_cntrl_dest="${_tmp_base}_cntrl"
+_tmp_files="${_stdout_dest} ${_stderr_dest} ${_output_dest} ${_cloyster_dest} ${_cntrl_dest}"
+
+_debug "tmp_files: ${_tmp_files}"
+for _f in ${_tmp_files}; do
+ if ! touch "${_f}"; then _execution_error error=tmpfile_creation_failed tmpfile="${_f}"; fi
+done
+
+# _logger_pretty_check NAME ISO806ZDATE LINE...
+_logger_pretty_echo() {
+ _name=$1
+ _date=$2
+ shift 2
+ _line=$@
+ _time=$(echo "$_date" | cut -d 'T' -f 2)
+ _bold=$(tput md)
+ _format_rst=$(tput me)
+ _color=$(tput AF 3)
+ _time_color=$(tput AF 59)
+ case "${_name}" in
+ stdout) _color=$(tput AF 2);;
+ stderr) _color=$(tput AF 1);;
+ cloyster) _color=$(tput AF 111);;
+ esac
+ >&2 echo -e "${_time_color}${_time} ${_color}${_bold}[${_name}]${_format_rst} ${_line}"
+}
+
+_logger_pids=""
+_json_logger() {
+ _name=$1
+ _log=$2
+ _format=$3
+ _combined_log=${4:-${_output_dest}}
+ _json_logger_job() {
+ _debug "_json_logger ${_name} ${_log}"
+ tail -F "${_log}" | while read -r _line; do
+ _date=$(date_now_iso_utc)
+ case "${_format}" in
+ text)
+ echo "${_line}" | jo "output=${_name}" "date=${_date}" "line=@-" >> "${_combined_log}"
+ _logger_pretty_echo "${_name}" "${_date}" "${_line}"
+ ;;
+ rawjson)
+ echo "${_line}" >> "${_combined_log}"
+ _date=$(echo "${_line}" | jq -r '.date')
+ _logger_pretty_echo "${_name}" "${_date}" "${_line}"
+ ;;
+ esac
+ done
+ }
+ $(_json_logger_job) &
+ _logger_pids="${_logger_pids} $!"
+}
+_json_loggers_stop() {
+ for tailpid in $(ps auxww | grep "tail -F ${_tmp_base}" | grep -v grep | awk '{ print $2 }'); do
+ kill -s PIPE "${tailpid}" || true
+ done
+ for pid in ${_logger_pids}; do
+ kill "${pid}" > /dev/null 2>&1 || true
+ wait "${pid}" 2>/dev/null || true;
+ done
+}
+_json_logger "cloyster" "${_cloyster_dest}" "rawjson"
+_json_logger "stdout" "${_stdout_dest}" "text"
+_json_logger "stderr" "${_stderr_dest}" "text"
+
+_exec_post() {
+ trap - EXIT
+ _arg=$1
+ _executing=
+ _debug "_exec_post $@"
+ if ! is_true "${XTRACE}" && is_true "${EXEC_XTRACE}"; then set -x; fi
+ end_iso=$(date_now_iso_utc)
+ end_us=$(posix_time_microseconds)
+ duration=$(bc -e "$end_us - $start_us")
+ _json_loggers_stop
+ cat ${_output_dest} | jq -sr '. | @json' > "${_output_dest}.json"
+ eval $(cat $_cntrl_dest)
+ _format_output
+ rm "${_tmp_base}"* || true
+ if [ ! "${_arg}" = "TRAPEXIT" ]; then exit "${_exec_exit_status:-1}"; fi
+}
+_format_output() {
+ jo "hostname=${hostname}" \
+ "command=${_exec}" \
+ "arguments=$(json_unquote ${_args_json:-'{}'})" \
+ "error=${_exec_error:-false}" \
+ "exit_status=${_exec_exit_status}" \
+ "output=:${_output_dest}.json" \
+ "result=${_exec_result}" \
+ "started_at_utc=${start_iso}" \
+ "finished_at_utc=${end_iso}" \
+ "duration_microseconds=${duration}"
+}
+
+_exec_pre() {
+ start_iso=$(date_now_iso_utc)
+ start_us=$(posix_time_microseconds)
+ if ! is_true "${XTRACE}" && is_true "${EXEC_XTRACE}"; then set -x; fi
+ _executing=y
+ trap - EXIT
+}
+
+_exec__trap_exit() {
+ _exit_status=$?
+ _debug "Exec process exited: ${_exit_status}"
+ _exec__put_return "exit_status" "${_exit_status}"
+ if [ ! "${_exit_status}" = "0" ] && ! grep -E "^_exec_error=" ${_cntrl_dest} > /dev/null; then
+ _exec__put_return "error" "$(jo type=command error=exit_status)" "json"
+ fi
+}
+
+_exec__put_return() {
+ _key=$1
+ _value=$2
+ _format=$3
+ if [ "${_format}" = "json" ]; then
+ _value="$(json_quote ${_value})"
+ fi
+ echo "_exec_${_key}=${_value}" >> ${_cntrl_dest}
+}
+
+_debug "executing: command=${_exec} args=${@}"
+_exec_pre
+if is_true "$stdin"; then
+ eval "$(cat -)" 2> "${_stderr_dest}" > "${_stdout_dest}"
+else
+ SCRIPT="${_exec}"
+ # shellcheck source=/dev/null
+ eval "$(
+ trap _exec__trap_exit EXIT
+ _execution_error() {
+ _exec__put_return "error" "$( jo type=cmd $@)" "json"
+ exit 2
+ }
+ return_result() {
+ _exec__put_return "result" "$1" "json"
+ exit 0
+ }
+ . "${SCRIPT}" 2>"${_stderr_dest}" 1>"${_stdout_dest}"
+ )" || true
+fi
+_exec_post