Added sessions

This commit is contained in:
Ben Ashton 2023-03-06 02:07:09 -07:00
parent b8a8b0e258
commit 29d58016e0
7 changed files with 296 additions and 64 deletions

View File

@ -1,10 +1,24 @@
#!/bin/bash #!/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 "$file_dir/tests/utils.sh"
source "$(dirname "$0")/tests/routes.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; testSummary;

View File

@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
# shellcheck disable=SC2034 # false flags nameref params # shellcheck disable=SC2034 # false flags nameref params
# shellcheck disable=SC2178 # false flags nameref params
# ====================== # ======================
# Required shell options # Required shell options
@ -172,6 +173,12 @@ function lenQuery() {
_multiLen _exprashQuery "$1" _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 # $1: key
# $2: (optional) index for accessing array parameters # $2: (optional) index for accessing array parameters
function body() { function body() {
@ -186,11 +193,18 @@ function lenBody() {
_multiLen _exprashBody "$1" _multiLen _exprashBody "$1"
} }
# Call this function to parse URLencoded request bodies # $1: key
function bodyParser() { function cookie() {
if [[ "${HTTP_CONTENT_TYPE,,}" == "application/x-www-form-urlencoded" ]]; then printf '%s' "${_exprashCookies[$1]}"
_parseUrlEncoded _exprashBody }
fi # $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 [ "$key" == 'Content-Type' ] && continue
printf '%s\n' "${key}: ${_exprashHeaders[$key]}" | _sendRaw printf '%s\n' "${key}: ${_exprashHeaders[$key]}" | _sendRaw
done; done;
for key in "${!_exprashSetCookies[@]}"; do
printf '%s\n' "Set-Cookie: ${key}=${_exprashSetCookies[$key]}" | _sendRaw
done;
printf '\n' | _sendRaw 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 # Internals
# ========= # =========
@ -287,7 +340,7 @@ function _pathMatch() {
local pathComponent="${pathArr[$i]}"; local pathComponent="${pathArr[$i]}";
# If route component starts with ":" # If route component starts with ":"
if [[ "$routeComponent" == :* ]]; then if [[ "$routeComponent" == :* ]] && [ -n "$pathComponent" ]; then
routeParams["${routeComponent:1}"]="$pathComponent"; routeParams["${routeComponent:1}"]="$pathComponent";
elif [[ "$routeComponent" == '*' ]] && [ -n "$pathComponent" ]; then elif [[ "$routeComponent" == '*' ]] && [ -n "$pathComponent" ]; then
continue; continue;
@ -315,7 +368,7 @@ function _multiAdd() {
local value="$3" local value="$3"
local i=0 local i=0
while [[ -v "multiArr[$i,$key]" ]]; do while [[ -v "multiArr[$i,$key]" ]]; do
let i++ (( i++ ))
done done
multiArr["$i,$key"]="$value" multiArr["$i,$key"]="$value"
} }
@ -325,10 +378,9 @@ function _multiAdd() {
function _multiLen() { function _multiLen() {
local -n multiArr="$1" local -n multiArr="$1"
local key="$2" local key="$2"
local value="$3"
local i=0 local i=0
while [[ -v "multiArr[$i,$key]" ]]; do while [[ -v "multiArr[$i,$key]" ]]; do
let i++ (( i++ ))
done done
printf '%s' "$i" printf '%s' "$i"
} }
@ -355,15 +407,19 @@ function _multiGet() {
# URL Encoded Parser # URL Encoded Parser
# ================== # ==================
# $1: urlencoded string # $1 | stdin: urlencoded string
decodeUri () { decodeUri () {
local i="${*//+/ }"; local input_str="${1-"$(< /dev/stdin)"}"
echo -e "${i//%/\\x}"; 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() { function _parseUrlEncoded() {
local -n parsedArr="$1" local -n parsedArr="$1"
local url_encoded_str="${2-"$(< /dev/stdin)"}"
local pair name value
while IFS= read -d '&' -r pair || [ "$pair" ]; do while IFS= read -d '&' -r pair || [ "$pair" ]; do
name=$(decodeUri "${pair%%=*}") name=$(decodeUri "${pair%%=*}")
@ -371,48 +427,140 @@ function _parseUrlEncoded() {
if [ -n "$name" ]; then if [ -n "$name" ]; then
_multiAdd parsedArr "$name" "$value" _multiAdd parsedArr "$name" "$value"
fi; fi;
done done <<< "$url_encoded_str"
} }
# ======= # =======
# Globals # Cookies
# ======= # =======
# Setup globals # $1: (nameref) associative array
_exprashRedirectStdout='' # $2 | stdin: cookie string to parse
function _parseCookies() {
local -n parsedArr="$1"
local cookie_str="${2-"$(< /dev/stdin)"}"
# Route Parameters local pair name value
declare -gA _exprashParams
# Body Parameters while IFS= read -d ';' -r pair || [ "$pair" ]; do
declare -gA _exprashBody name="$(_trim "${pair%%=*}" | decodeUri)"
value="$(_trim "${pair#*=}" | decodeUri)"
if [ -n "$name" ]; then
parsedArr["$name"]="$value"
fi
done <<< "$cookie_str"
}
# Query Parameters # =======
declare -gA _exprashQuery # Session
# =======
# Headers function _loadSession() {
declare -gA _exprashHeaders [ "$_exprashUseSession" -eq 1 ] || return 1
[ -n "$_exprashSessionDir" ] || return 1
hasCookie "$_exprashSessionCookieName" || return 1
function _exprashResetRouteGlobals() { local session_id
_exprashParams=() session_id="$(cookie "$_exprashSessionCookieName")"
_exprashBody=() local session_file="${_exprashSessionDir%/}/$session_id.session"
_exprashQuery=()
_exprashHeaders=() # 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"
# Set globals
_exprashSessionId=$session_id
_exprashSession=()
}
function _saveSession() {
[ "$_exprashUseSession" -eq 1 ] || return 1
[ -n "$_exprashSessionDir" ] || return 1
[ -n "$_exprashSessionId" ] || return 1
local session_file="${_exprashSessionDir%/}/${_exprashSessionId}.session"
declare -p _exprashSession | sed '1 s/\([^-]*-\)/\1g/' > "$session_file"
}
# =================
# 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 _exprashRouteHandled=0
_exprashErrorMessage='' _exprashErrorMessage=''
_exprashHeadersSent=0 _exprashHeadersSent=0
_exprashHeaders['Content-Type']='text/html' _exprashHeaders['Content-Type']='text/html'
# Parse query string
_parseUrlEncoded _exprashQuery "$QUERY_STRING"
# Parse cookies
_parseCookies _exprashCookies "$HTTP_COOKIE"
} }
# Set up route globals # Shutdown trap
_exprashResetRouteGlobals trap _exprashShutdown EXIT
# ============== # Initialize exprash
# Initialization _exprashInit
# ==============
# Parse query string
_parseUrlEncoded _exprashQuery < <(echo "$QUERY_STRING")

20
tests/cookies.sh Executable file
View File

@ -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'
})"

6
tests/query.sh Normal file
View File

@ -0,0 +1,6 @@
it "Should parse query string" "$({
QUERY_STRING='ohmy=zsh&exit=vim'
_exprashInit
hasQuery 'ohmy' && ! hasQuery 'ohno' && [ "$(query 'exit')" == 'vim' ]
})"

View File

@ -1,55 +1,62 @@
it "Should match plain route" $({ it "Should match plain route" $({
_exprashResetRouteGlobals
PATH_INFO='/plain/route' PATH_INFO='/plain/route'
_exprashInit
all '/plain/route' all '/plain/route'
}) })
it "Should not match incorrect plain route" $({ it "Should not match incorrect plain route" $({
_exprashResetRouteGlobals
PATH_INFO='/plain/WRONG' PATH_INFO='/plain/WRONG'
_exprashInit
! all '/plain/route' ! all '/plain/route'
}) })
it "Should extract parameter" $({ it "Should extract parameter" $({
_exprashResetRouteGlobals
PATH_INFO='/cats/calico/pet' PATH_INFO='/cats/calico/pet'
_exprashInit
all '/cats/:cat/pet' && hasParam 'cat' && [ "$(param 'cat')" == 'calico' ] all '/cats/:cat/pet' && hasParam 'cat' && [ "$(param 'cat')" == 'calico' ]
}) })
it "Should match wildcard route" $({ it "Should match wildcard route" $({
_exprashResetRouteGlobals
PATH_INFO='/cats/calico/pet' PATH_INFO='/cats/calico/pet'
_exprashInit
all '/cats/*/pet' all '/cats/*/pet'
}) })
it "Should not match incorrect wildcard route" $({ it "Should not match incorrect wildcard route" $({
_exprashResetRouteGlobals
PATH_INFO='/cats/calico/' PATH_INFO='/cats/calico/'
_exprashInit
! all '/cats/*/pet' ! all '/cats/*/pet'
}) })
it "Should match multi-wildcard route" $({ it "Should match multi-wildcard route" $({
_exprashResetRouteGlobals
PATH_INFO='/cats/calico/pet/donkey' PATH_INFO='/cats/calico/pet/donkey'
_exprashInit
all '/cats/**' all '/cats/**'
}) })
it "Should not match incorrect multi-wildcard route" $({ it "Should not match incorrect multi-wildcard route" $({
_exprashResetRouteGlobals
PATH_INFO='/INCORRECT/calico/pet/donkey' PATH_INFO='/INCORRECT/calico/pet/donkey'
_exprashInit
! all '/cats/**' ! all '/cats/**'
}) })
it "Should not match path shorter than route" $({
PATH_INFO='/year'
_exprashInit
! all '/year/:year'
})
it "Should match get route" $({ it "Should match get route" $({
_exprashResetRouteGlobals
REQUEST_METHOD='GET' REQUEST_METHOD='GET'
PATH_INFO='/simple/route' PATH_INFO='/simple/route'
_exprashInit
get '/simple/route' get '/simple/route'
}) })
it "Should not match get route with incorrect method" $({ it "Should not match get route with incorrect method" $({
_exprashResetRouteGlobals
REQUEST_METHOD='POST' REQUEST_METHOD='POST'
PATH_INFO='/simple/route' PATH_INFO='/simple/route'
_exprashInit
! get '/simple/route' ! get '/simple/route'
}) })

30
tests/session.sh Normal file
View File

@ -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" ]
})"

View File

@ -1,23 +1,30 @@
_testPassCount=0; _testPassCount=0
_testTotalCount=0; _testTotalCount=0
# $1: Message
# $2: Subshell output
function it() { function it() {
local exitCode=$?; local exitCode=$?
local message=$1; local message="$1"
local status; local output="$2"
local status
_testTotalCount=$((_testTotalCount+1)); _testTotalCount=$((_testTotalCount+1))
if [ "$exitCode" -eq 0 ]; then if [ "$exitCode" -eq 0 ]; then
status='✓'; status='✓'
_testPassCount=$((_testPassCount+1)); _testPassCount=$((_testPassCount+1))
else else
status='✗'; status='✗'
fi; fi
printf '%s %s\n' "$status" "$message"; if [ -n "$output" ]; then
printf '%s\n' "$output"
fi
printf '%s %s\n' "$status" "$message"
} }
function testSummary() { function testSummary() {
printf 'Passed: %s of %s tests\n' "$_testPassCount" "$_testTotalCount"; printf 'Passed: %s of %s tests\n' "$_testPassCount" "$_testTotalCount"
} }