From 29d58016e0b5cf2e230ed0bffa9fef5da019335d Mon Sep 17 00:00:00 2001 From: Ben Ashton Date: Mon, 6 Mar 2023 02:07:09 -0700 Subject: [PATCH] Added sessions --- run_tests.sh | 24 +++-- src/exprash.sh | 222 +++++++++++++++++++++++++++++++++++++++-------- tests/cookies.sh | 20 +++++ tests/query.sh | 6 ++ tests/routes.sh | 25 ++++-- tests/session.sh | 30 +++++++ tests/utils.sh | 31 ++++--- 7 files changed, 295 insertions(+), 63 deletions(-) create mode 100755 tests/cookies.sh create mode 100644 tests/query.sh create mode 100644 tests/session.sh diff --git a/run_tests.sh b/run_tests.sh index d6fdc99..1e35c74 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -1,10 +1,24 @@ #!/bin/bash -source "$(dirname "$0")/src/exprash.sh"; +file_dir="$(dirname "$0")" -source "$(dirname "$0")/tests/utils.sh"; +source "$file_dir/src/exprash.sh" -printf '%s\n' "Routes:"; -source "$(dirname "$0")/tests/routes.sh"; +source "$file_dir/tests/utils.sh" + +printf '%s\n' "Routes:" +source "$file_dir/tests/routes.sh" +printf '\n' + +printf '%s\n' "Query String:" +source "$file_dir/tests/query.sh" +printf '\n' + +printf '%s\n' "Cookies:" +source "$file_dir/tests/cookies.sh" +printf '\n' + +printf '%s\n' "Session:" +source "$file_dir/tests/session.sh" +printf '\n' -printf '\n'; testSummary; diff --git a/src/exprash.sh b/src/exprash.sh index 9f3e204..c104952 100644 --- a/src/exprash.sh +++ b/src/exprash.sh @@ -1,5 +1,6 @@ #!/bin/bash # shellcheck disable=SC2034 # false flags nameref params +# shellcheck disable=SC2178 # false flags nameref params # ====================== # Required shell options @@ -172,6 +173,12 @@ function lenQuery() { _multiLen _exprashQuery "$1" } +# Call this function to parse URLencoded request bodies +function useBody() { + if [[ "${HTTP_CONTENT_TYPE,,}" == "application/x-www-form-urlencoded" ]]; then + _parseUrlEncoded _exprashBody + fi +} # $1: key # $2: (optional) index for accessing array parameters function body() { @@ -186,11 +193,18 @@ function lenBody() { _multiLen _exprashBody "$1" } -# Call this function to parse URLencoded request bodies -function bodyParser() { - if [[ "${HTTP_CONTENT_TYPE,,}" == "application/x-www-form-urlencoded" ]]; then - _parseUrlEncoded _exprashBody - fi +# $1: key +function cookie() { + printf '%s' "${_exprashCookies[$1]}" +} +# $1: key +function hasCookie() { + [[ -v "_exprashCookies[$1]" ]] +} +# $1: key +# $2: value +function setCookie() { + _exprashSetCookies["$1"]="$2" } # ================== @@ -240,9 +254,48 @@ function sendHeaders() { [ "$key" == 'Content-Type' ] && continue printf '%s\n' "${key}: ${_exprashHeaders[$key]}" | _sendRaw done; + for key in "${!_exprashSetCookies[@]}"; do + printf '%s\n' "Set-Cookie: ${key}=${_exprashSetCookies[$key]}" | _sendRaw + done; printf '\n' | _sendRaw } + +# ======= +# Session +# ======= + +# Call this function to automatically manage sessions +# $1: (optional) session dir, defaults to "session" in the current directory +function useSession() { + local session_dir=$1 + + # Create default session directory + if [ -z "$session_dir" ]; then + session_dir="./session" + mkdir -p "$session_dir" + fi + + # Setup session globals + _exprashUseSession=1 + _exprashSessionDir=$session_dir + + _loadSession || _createSession || return 1 +} + +# Set a session variable +# $1: variable name +# $2: (optional) vairable value +function session() { + local name="$1" + if [[ -v 2 ]]; then + local value="$2" + _exprashSession["$name"]="$value" + else + printf '%s' "${_exprashSession["$name"]}" + fi +} + # ========= # Internals # ========= @@ -287,7 +340,7 @@ function _pathMatch() { local pathComponent="${pathArr[$i]}"; # If route component starts with ":" - if [[ "$routeComponent" == :* ]]; then + if [[ "$routeComponent" == :* ]] && [ -n "$pathComponent" ]; then routeParams["${routeComponent:1}"]="$pathComponent"; elif [[ "$routeComponent" == '*' ]] && [ -n "$pathComponent" ]; then continue; @@ -315,7 +368,7 @@ function _multiAdd() { local value="$3" local i=0 while [[ -v "multiArr[$i,$key]" ]]; do - let i++ + (( i++ )) done multiArr["$i,$key"]="$value" } @@ -325,10 +378,9 @@ function _multiAdd() { function _multiLen() { local -n multiArr="$1" local key="$2" - local value="$3" local i=0 while [[ -v "multiArr[$i,$key]" ]]; do - let i++ + (( i++ )) done printf '%s' "$i" } @@ -355,15 +407,19 @@ function _multiGet() { # URL Encoded Parser # ================== -# $1: urlencoded string +# $1 | stdin: urlencoded string decodeUri () { - local i="${*//+/ }"; - echo -e "${i//%/\\x}"; + local input_str="${1-"$(< /dev/stdin)"}" + input_str="${input_str//+/ }"; + echo -e "${input_str//%/\\x}"; } -# $1: multi associative array +# $1: (nameref) multi associative array +# $2 | stdin: url encoded data function _parseUrlEncoded() { local -n parsedArr="$1" + local url_encoded_str="${2-"$(< /dev/stdin)"}" + local pair name value while IFS= read -d '&' -r pair || [ "$pair" ]; do name=$(decodeUri "${pair%%=*}") @@ -371,48 +427,140 @@ function _parseUrlEncoded() { if [ -n "$name" ]; then _multiAdd parsedArr "$name" "$value" fi; - done + done <<< "$url_encoded_str" } +# ======= +# Cookies +# ======= + +# $1: (nameref) associative array +# $2 | stdin: cookie string to parse +function _parseCookies() { + local -n parsedArr="$1" + local cookie_str="${2-"$(< /dev/stdin)"}" + + local pair name value + + while IFS= read -d ';' -r pair || [ "$pair" ]; do + name="$(_trim "${pair%%=*}" | decodeUri)" + value="$(_trim "${pair#*=}" | decodeUri)" + if [ -n "$name" ]; then + parsedArr["$name"]="$value" + fi + done <<< "$cookie_str" +} # ======= -# Globals +# Session # ======= -# Setup globals -_exprashRedirectStdout='' +function _loadSession() { + [ "$_exprashUseSession" -eq 1 ] || return 1 + [ -n "$_exprashSessionDir" ] || return 1 + hasCookie "$_exprashSessionCookieName" || return 1 + + local session_id + session_id="$(cookie "$_exprashSessionCookieName")" + local session_file="${_exprashSessionDir%/}/$session_id.session" + + # shellcheck disable=SC1090 + source "$session_file" || return 1 + + # Set globals + _exprashSessionId=$session_id +} + +function _createSession() { + [ "$_exprashUseSession" -eq 1 ] || return 1 + [ -n "$_exprashSessionDir" ] || return 1 + + # mktemp args + local args=() + args+=('-p' "$_exprashSessionDir") + args+=("$(printf 'X%.0s' {1..32}).session") + + local session_file + session_file=$(mktemp -u "${args[@]}") || return 1 + local session_file_name=${session_file##*/} + local session_id=${session_file_name%%.*} + + # Set cookie + setCookie "$_exprashSessionCookieName" "$session_id" -# Route Parameters -declare -gA _exprashParams + # Set globals + _exprashSessionId=$session_id + _exprashSession=() +} -# Body Parameters -declare -gA _exprashBody +function _saveSession() { + [ "$_exprashUseSession" -eq 1 ] || return 1 + [ -n "$_exprashSessionDir" ] || return 1 + [ -n "$_exprashSessionId" ] || return 1 -# Query Parameters -declare -gA _exprashQuery + local session_file="${_exprashSessionDir%/}/${_exprashSessionId}.session" -# Headers -declare -gA _exprashHeaders + declare -p _exprashSession | sed '1 s/\([^-]*-\)/\1g/' > "$session_file" +} -function _exprashResetRouteGlobals() { - _exprashParams=() - _exprashBody=() - _exprashQuery=() - _exprashHeaders=() +# ================= +# Utility Functions +# ================= + +# $1 | stdin: string to trim +function _trim() { + local str="${1-"$(< /dev/stdin)"}" + + # trim leading spaces + str="${str#"${str%%[![:space:]]*}"}" + # trim trailing spaces + str="${str%"${str##*[![:space:]]}"}" + + printf '%s' "$str" +} + +# ======== +# Shutdown +# ======== + +function _exprashShutdown() { + _saveSession +} + +# ============== +# Initialization +# ============== +function _exprashInit() { + _exprashRedirectStdout='' + + declare -gA _exprashParams=() + declare -gA _exprashBody=() + declare -gA _exprashQuery=() + declare -gA _exprashHeaders=() + declare -gA _exprashCookies=() + declare -gA _exprashSetCookies=() + + _exprashUseSession=0 + _exprashSessionDir='' + _exprashSessionId='' + declare -gA _exprashSession=() + _exprashSessionCookieName='exprash_session' _exprashRouteHandled=0 _exprashErrorMessage='' _exprashHeadersSent=0 _exprashHeaders['Content-Type']='text/html' + # Parse query string + _parseUrlEncoded _exprashQuery "$QUERY_STRING" + + # Parse cookies + _parseCookies _exprashCookies "$HTTP_COOKIE" } -# Set up route globals -_exprashResetRouteGlobals +# Shutdown trap +trap _exprashShutdown EXIT -# ============== -# Initialization -# ============== +# Initialize exprash +_exprashInit -# Parse query string -_parseUrlEncoded _exprashQuery < <(echo "$QUERY_STRING") diff --git a/tests/cookies.sh b/tests/cookies.sh new file mode 100755 index 0000000..597e4cb --- /dev/null +++ b/tests/cookies.sh @@ -0,0 +1,20 @@ +it "Should parse cookies" "$({ + HTTP_COOKIE='bob=gob; job=tob' + _exprashInit + output=$(declare -p _exprashCookies) + expected_output='declare -A _exprashCookies=([bob]="gob" [job]="tob" )' + [ "$output" == "$expected_output" ] +})" + +it "Should get cookie value by name" "$({ + HTTP_COOKIE='bob=gob; job=tob' + _exprashInit + [ "$(cookie 'bob')" == 'gob' ] +})" + +it "Should determine if a cookie exists by name" "$({ + HTTP_COOKIE='bob=gob; job=tob' + _exprashInit + hasCookie 'bob' && ! hasCookie 'kob' +})" + diff --git a/tests/query.sh b/tests/query.sh new file mode 100644 index 0000000..e3ffac6 --- /dev/null +++ b/tests/query.sh @@ -0,0 +1,6 @@ +it "Should parse query string" "$({ + QUERY_STRING='ohmy=zsh&exit=vim' + _exprashInit + hasQuery 'ohmy' && ! hasQuery 'ohno' && [ "$(query 'exit')" == 'vim' ] +})" + diff --git a/tests/routes.sh b/tests/routes.sh index 7a474c6..55d4a3e 100755 --- a/tests/routes.sh +++ b/tests/routes.sh @@ -1,55 +1,62 @@ it "Should match plain route" $({ - _exprashResetRouteGlobals PATH_INFO='/plain/route' + _exprashInit all '/plain/route' }) it "Should not match incorrect plain route" $({ - _exprashResetRouteGlobals PATH_INFO='/plain/WRONG' + _exprashInit ! all '/plain/route' }) it "Should extract parameter" $({ - _exprashResetRouteGlobals PATH_INFO='/cats/calico/pet' + _exprashInit all '/cats/:cat/pet' && hasParam 'cat' && [ "$(param 'cat')" == 'calico' ] }) it "Should match wildcard route" $({ - _exprashResetRouteGlobals PATH_INFO='/cats/calico/pet' + _exprashInit all '/cats/*/pet' }) it "Should not match incorrect wildcard route" $({ - _exprashResetRouteGlobals PATH_INFO='/cats/calico/' + _exprashInit ! all '/cats/*/pet' }) it "Should match multi-wildcard route" $({ - _exprashResetRouteGlobals PATH_INFO='/cats/calico/pet/donkey' + _exprashInit all '/cats/**' }) it "Should not match incorrect multi-wildcard route" $({ - _exprashResetRouteGlobals PATH_INFO='/INCORRECT/calico/pet/donkey' + _exprashInit ! all '/cats/**' }) +it "Should not match path shorter than route" $({ + PATH_INFO='/year' + _exprashInit + ! all '/year/:year' +}) + it "Should match get route" $({ - _exprashResetRouteGlobals REQUEST_METHOD='GET' PATH_INFO='/simple/route' + _exprashInit get '/simple/route' }) it "Should not match get route with incorrect method" $({ - _exprashResetRouteGlobals REQUEST_METHOD='POST' PATH_INFO='/simple/route' + _exprashInit ! get '/simple/route' }) + diff --git a/tests/session.sh b/tests/session.sh new file mode 100644 index 0000000..e258354 --- /dev/null +++ b/tests/session.sh @@ -0,0 +1,30 @@ +# shellcheck disable=SC2154,SC2034 + +testSessionDir='/tmp/exprash_test_sessions' +mkdir -p "$testSessionDir" + +it "Should set session directory" "$({ + _exprashInit + useSession "$testSessionDir" + [ "$_exprashSessionDir" == "$testSessionDir" ] +})" + +it "Should save and load session" "$({ + _exprashInit + useSession "$testSessionDir" + session 'data' 'value' + session_id=$_exprashSessionId + cookie_value=${_exprashSetCookies[$_exprashSessionCookieName]} + _exprashShutdown + + # Forcibly clear session data just in case (but it should also get cleared by + # _exprashInit) + _exprashSession=() + _exprashSessionId='' + + HTTP_COOKIE="${_exprashSessionCookieName}=$cookie_value" + _exprashInit + useSession "$testSessionDir" + [ "$(session 'data')" == "value" ] +})" + diff --git a/tests/utils.sh b/tests/utils.sh index e97c002..690452f 100755 --- a/tests/utils.sh +++ b/tests/utils.sh @@ -1,23 +1,30 @@ -_testPassCount=0; -_testTotalCount=0; +_testPassCount=0 +_testTotalCount=0 +# $1: Message +# $2: Subshell output function it() { - local exitCode=$?; - local message=$1; - local status; + local exitCode=$? + local message="$1" + local output="$2" + local status - _testTotalCount=$((_testTotalCount+1)); + _testTotalCount=$((_testTotalCount+1)) if [ "$exitCode" -eq 0 ]; then - status='✓'; - _testPassCount=$((_testPassCount+1)); + status='✓' + _testPassCount=$((_testPassCount+1)) else - status='✗'; - fi; + status='✗' + fi - printf '%s %s\n' "$status" "$message"; + if [ -n "$output" ]; then + printf '%s\n' "$output" + fi + printf '%s %s\n' "$status" "$message" } function testSummary() { - printf 'Passed: %s of %s tests\n' "$_testPassCount" "$_testTotalCount"; + printf 'Passed: %s of %s tests\n' "$_testPassCount" "$_testTotalCount" } +