From 75b395ffa25f77869f4bee9fc31f6bf925482bf7 Mon Sep 17 00:00:00 2001 From: Ben Ashton Date: Thu, 9 Mar 2023 17:44:11 -0700 Subject: [PATCH] Finished adding sessions and updating examples --- README.md | 8 +- examples/cats.sh | 8 +- examples/login.sh | 86 ++++++++++++ src/exprash.sh | 340 +++++++++++++++++++++++----------------------- tests/cookies.sh | 14 +- tests/query.sh | 6 +- tests/routes.sh | 64 ++++----- tests/session.sh | 32 ++--- 8 files changed, 327 insertions(+), 231 deletions(-) create mode 100644 examples/login.sh diff --git a/README.md b/README.md index 3ba4854..bb72bf0 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Here is some leaked source code for my brand new Cat website. Do not steal! #!/bin/bash source exprash.sh; -redirectStdout 'log'; +redirect_stdout 'log'; declare -A cats; cats[calico]="Calico"; @@ -50,11 +50,11 @@ get '/cats/*' && { printf '

Error: Cannot find that cat

\n' | send; } -getError '/cats/*' && { - printf '

%s

' "$(errorMessage)" | send; +get_error '/cats/*' && { + printf '

%s

' "$(get_error_message)" | send; } -(use || useError) && { +(use || use_error) && { printf '

404

' | send; } ``` diff --git a/examples/cats.sh b/examples/cats.sh index ad8f36d..f4dbc46 100644 --- a/examples/cats.sh +++ b/examples/cats.sh @@ -1,7 +1,7 @@ #!/bin/bash source exprash.sh; -redirectStdout 'log'; +redirect_stdout 'log'; declare -A cats; cats[calico]="Calico"; @@ -39,10 +39,10 @@ get '/cats/*' && { printf '

Error: Cannot find that cat

\n' | send; } -getError '/cats/*' && { - printf '

%s

' "$(errorMessage)" | send; +get_error '/cats/*' && { + printf '

%s

' "$(get_error_message)" | send; } -(use || useError) && { +(use || use_error) && { printf '

404

' | send; } diff --git a/examples/login.sh b/examples/login.sh new file mode 100644 index 0000000..8ef62ee --- /dev/null +++ b/examples/login.sh @@ -0,0 +1,86 @@ +#!/bin/bash +source exprash.sh; + +redirect_stdout 'log'; +use_session +use_body + +username='admin' +password='password' + +# Authorization middleware +function is_authorized() { + if [ "$(session 'authorized')" != "1" ]; then + next 'unauthorized' + return 1 + fi +} + +get '/' && { + if [ "$(session 'authorized')" == "1" ]; then + html="

Welcome $username

" + html+="Click Here For Secrets

" + html+="Logout" + else + html="

Welcome

" + html+="

You must login to learn secrets

" + html+="Login" + fi + printf '%s\n' "$html" | send +} + +get '/admin' && is_authorized && { + html='

Here are all of my secrets:

' + html+="" + html+="Go Home" + printf '%s' "$html" | send +} + +get '/login' && { + html="

Login:

" + html+='
' + html+=' ' + html+=' ' + html+=' ' + html+='
' + printf '%s\n' "$html" | send +} + +get '/incorrect-password' && { + if [ "$(session 'authorized')" == "1" ]; then + redirect '.' + else + html="

Incorrect Password

" + html+="

Try again:

" + html+="Login" + printf '%s\n' "$html" | send + fi +} + +post '/login' && { + post_user=$(body 'username') + post_pass=$(body 'password') + if [ "$post_user" == "$username" ] && [ "$post_pass" == "$password" ]; then + session 'authorized' 1 + redirect '.' + else + redirect 'incorrect-password' + fi +} + +get '/logout' && { + session 'authorized' 0 + redirect '.' +} + +(use || use_error) && { + if [ "$(get_error_message)" == "unauthorized" ]; then + html='

Error: Access Denied

' + html+='Click here to login' + printf '%s' "$html" | send + else + status '404' + printf '

404

' | send + fi +} + diff --git a/src/exprash.sh b/src/exprash.sh index c104952..6f1efbd 100644 --- a/src/exprash.sh +++ b/src/exprash.sh @@ -14,13 +14,13 @@ shopt -s lastpipe # =============== # $1: Log file for redirecting stdout to -function redirectStdout() { +function redirect_stdout() { local output="$1"; [ -z "$output" ] && output="/dev/null"; exec 5<&1; - _exprashRedirectStdout="$output"; - exec 1>>"$_exprashRedirectStdout"; + _exprash_redirect_stdout="$output"; + exec 1>>"$_exprash_redirect_stdout"; printf '%s: %s\n' "${REQUEST_METHOD:-UNKNOWN}" "${PATH_INFO:-/}" } @@ -54,23 +54,23 @@ function delete() { # $1: path function all() { - [ "$_exprashRouteHandled" -eq 1 ] && return 1; - [ -n "$_exprashErrorMessage" ] && return 1; + [ "$_exprash_route_handled" -eq 1 ] && return 1; + [ -n "$_exprash_error_message" ] && return 1; # Reset params - _exprashParams=(); + _exprash_params=(); - _pathMatch "$1" _exprashParams || return 1; - _exprashRouteHandled=1; + _path_match "$1" _exprash_params || return 1; + _exprash_route_handled=1; return 0; } function use() { - [ "$_exprashRouteHandled" -eq 1 ] && return 1; - [ -n "$_exprashErrorMessage" ] && return 1; + [ "$_exprash_route_handled" -eq 1 ] && return 1; + [ -n "$_exprash_error_message" ] && return 1; - _exprashRouteHandled=1; + _exprash_route_handled=1; return 0; } @@ -79,48 +79,48 @@ function use() { # ===================== # $1: path -function getError() { +function get_error() { [ "$REQUEST_METHOD" != "GET" ] && return 1; - allError "$1"; + all_error "$1"; } # $1: path -function postError() { +function post_error() { [ "$REQUEST_METHOD" != "POST" ] && return 1; - allError "$1"; + all_error "$1"; } # $1: path -function putError() { +function put_error() { [ "$REQUEST_METHOD" != "PUT" ] && return 1; - allError "$1"; + all_error "$1"; } # $1: path -function deleteError() { +function delete_error() { [ "$REQUEST_METHOD" != "DELETE" ] && return 1; - allError "$1"; + all_error "$1"; } # $1: path -function allError() { - [ "$_exprashRouteHandled" -eq 1 ] && return 1; - [ -z "$_exprashErrorMessage" ] && return 1; +function all_error() { + [ "$_exprash_route_handled" -eq 1 ] && return 1; + [ -z "$_exprash_error_message" ] && return 1; # Reset params - _exprashParams=(); + _exprash_params=(); - _pathMatch "$1" _exprashParams || return 1; - _exprashRouteHandled=1; + _path_match "$1" _exprash_params || return 1; + _exprash_route_handled=1; return 0; } -function useError() { - [ "$_exprashRouteHandled" -eq 1 ] && return 1; - [ -z "$_exprashErrorMessage" ] && return 1; +function use_error() { + [ "$_exprash_route_handled" -eq 1 ] && return 1; + [ -z "$_exprash_error_message" ] && return 1; - _exprashRouteHandled=1; + _exprash_route_handled=1; return 0; } @@ -130,20 +130,20 @@ function useError() { # $1 (optional): error message function next() { - _exprashRouteHandled=0; - [ -n "$1" ] && _exprashErrorMessage="$1"; + _exprash_route_handled=0; + [ -n "$1" ] && _exprash_error_message="$1"; } # ============= # App Functions # ============= -function errorMessage() { - printf '%s' "$_exprashErrorMessage"; +function get_error_message() { + printf '%s' "$_exprash_error_message"; } -function hasErrorMessage() { - [ -n "$_exprashErrorMessage" ]; +function has_error_message() { + [ -n "$_exprash_error_message" ]; } # ================= @@ -152,59 +152,60 @@ function hasErrorMessage() { # $1: param name function param() { - printf '%s\n' "${_exprashParams[$1]}"; + printf '%s\n' "${_exprash_params[$1]}"; } -function hasParam() { - [[ -v "_exprashParams[$1]" ]]; +function has_param() { + [[ -v "_exprash_params[$1]" ]]; } # $1: key # $2: (optional) index for accessing array parameters function query() { - _multiGet _exprashQuery "$1" "$2" + _multi_get _exprash_query "$1" "$2" } # $1: key -function hasQuery() { - _multiHas _exprashQuery "$1" +function has_query() { + _multi_has _exprash_query "$1" } # $1: key -function lenQuery() { - _multiLen _exprashQuery "$1" +function len_query() { + _multi_len _exprash_query "$1" } # Call this function to parse URLencoded request bodies -function useBody() { - if [[ "${HTTP_CONTENT_TYPE,,}" == "application/x-www-form-urlencoded" ]]; then - _parseUrlEncoded _exprashBody +function use_body() { + if [[ "${HTTP_CONTENT_TYPE,,}" == "application/x-www-form-urlencoded" ]]; + then + _parse_url_encoded _exprash_body fi } # $1: key # $2: (optional) index for accessing array parameters function body() { - _multiGet _exprashBody "$1" "$2" + _multi_get _exprash_body "$1" "$2" } # $1: key -function hasBody() { - _multiHas _exprashBody "$1" +function has_body() { + _multi_has _exprash_body "$1" } # $1: key -function lenBody() { - _multiLen _exprashBody "$1" +function len_body() { + _multi_len _exprash_body "$1" } # $1: key function cookie() { - printf '%s' "${_exprashCookies[$1]}" + printf '%s' "${_exprash_cookies[$1]}" } # $1: key -function hasCookie() { - [[ -v "_exprashCookies[$1]" ]] +function has_cookie() { + [[ -v "_exprash_cookies[$1]" ]] } # $1: key # $2: value -function setCookie() { - _exprashSetCookies["$1"]="$2" +function set_cookie() { + _exprash_set_cookies["$1"]="$2" } # ================== @@ -212,16 +213,16 @@ function setCookie() { # ================== function send() { - if [ "$_exprashHeadersSent" -eq 0 ]; then - sendHeaders - _exprashHeadersSent=1 + if [ "$_exprash_headers_sent" -eq 0 ]; then + send_headers + _exprash_headers_sent=1 fi - _sendRaw + _send_raw } -function sendJson() { - setHeader 'Content-Type' 'application/json' +function send_json() { + set_header 'Content-Type' 'application/json' send } @@ -233,31 +234,34 @@ function redirect() { fi status '302' - setHeader 'Location' "$path" + set_header 'Location' "$path" printf 'Redirecting to: %s' "$path" | send } # $1: satus code function status() { - setHeader "Status" "$1" + set_header "Status" "$1" } # $1: Header Name # $2: Header Value -function setHeader() { - _exprashHeaders["$1"]="$2" +function set_header() { + _exprash_headers["$1"]="$2" } -function sendHeaders() { - printf '%s\n' "Content-Type: ${_exprashHeaders['Content-Type']}" | _sendRaw - for key in "${!_exprashHeaders[@]}"; do +function send_headers() { + printf '%s\n' "Content-Type: ${_exprash_headers['Content-Type']}" | \ + _send_raw + for key in "${!_exprash_headers[@]}"; do [ "$key" == 'Content-Type' ] && continue - printf '%s\n' "${key}: ${_exprashHeaders[$key]}" | _sendRaw + printf '%s\n' "${key}: ${_exprash_headers[$key]}" | \ + _send_raw done; - for key in "${!_exprashSetCookies[@]}"; do - printf '%s\n' "Set-Cookie: ${key}=${_exprashSetCookies[$key]}" | _sendRaw + for key in "${!_exprash_set_cookies[@]}"; do + printf '%s\n' "Set-Cookie: ${key}=${_exprash_set_cookies[$key]}" | \ + _send_raw done; - printf '\n' | _sendRaw + printf '\n' | _send_raw } @@ -267,7 +271,7 @@ function sendHeaders() { # Call this function to automatically manage sessions # $1: (optional) session dir, defaults to "session" in the current directory -function useSession() { +function use_session() { local session_dir=$1 # Create default session directory @@ -277,10 +281,10 @@ function useSession() { fi # Setup session globals - _exprashUseSession=1 - _exprashSessionDir=$session_dir + _exprash_use_session=1 + _exprash_session_dir=$session_dir - _loadSession || _createSession || return 1 + _load_session || _create_session || return 1 } # Set a session variable @@ -290,9 +294,9 @@ function session() { local name="$1" if [[ -v 2 ]]; then local value="$2" - _exprashSession["$name"]="$value" + _exprash_session["$name"]="$value" else - printf '%s' "${_exprashSession["$name"]}" + printf '%s' "${_exprash_session["$name"]}" fi } @@ -300,21 +304,21 @@ function session() { # Internals # ========= -function _sendRaw() { - [ -n "$_exprashRedirectStdout" ] && exec >&5; +function _send_raw() { + [ -n "$_exprash_redirect_stdout" ] && exec >&5; cat; - [ -n "$_exprashRedirectStdout" ] && exec 1>>"$_exprashRedirectStdout"; + [ -n "$_exprash_redirect_stdout" ] && exec 1>>"$_exprash_redirect_stdout"; } # $1: path # $2: (nameref) array -function _pathToArray() { +function _path_to_array() { readarray -t "$2" < <(printf '%s\n' "$1" | tr '/' '\n' | grep .); } # $1: route # $2: (nameref) associative array for params -function _pathMatch() { +function _path_match() { [ -z ${PATH_INFO+x} ] && return 1; [ -z ${1+x} ] && return 1; @@ -322,33 +326,33 @@ function _pathMatch() { local route="$1"; # Params associative array - local -n routeParams="$2"; + local -n route_params="$2"; - local pathArr; - _pathToArray "$path" pathArr; + local path_arr; + _path_to_array "$path" path_arr; - local routeArr; - _pathToArray "$route" routeArr; + local route_arr; + _path_to_array "$route" route_arr; # Get max path length - local routeLen=${#routeArr[@]}; - local pathLen=${#pathArr[@]}; - local maxLen=$(( routeLen >= pathLen ? routeLen : pathLen )); + local route_len=${#route_arr[@]}; + local path_len=${#path_arr[@]}; + local maxLen=$(( route_len >= path_len ? route_len : path_len )); for ((i=0; i "$session_file" + declare -p _exprash_session | sed '1 s/\([^-]*-\)/\1g/' > "$session_file" } # ================= @@ -523,44 +527,44 @@ function _trim() { # Shutdown # ======== -function _exprashShutdown() { - _saveSession +function _exprash_shutdown() { + _save_session } # ============== # 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' +function _exprash_init() { + _exprash_redirect_stdout='' + + declare -gA _exprash_params=() + declare -gA _exprash_body=() + declare -gA _exprash_query=() + declare -gA _exprash_headers=() + declare -gA _exprash_cookies=() + declare -gA _exprash_set_cookies=() + + _exprash_use_session=0 + _exprash_session_dir='' + _exprash_sessionId='' + declare -gA _exprash_session=() + _exprash_session_cookie_name='exprash_session' + + _exprash_route_handled=0 + _exprash_error_message='' + _exprash_headers_sent=0 + _exprash_headers['Content-Type']='text/html' # Parse query string - _parseUrlEncoded _exprashQuery "$QUERY_STRING" + _parse_url_encoded _exprash_query "$QUERY_STRING" # Parse cookies - _parseCookies _exprashCookies "$HTTP_COOKIE" + _parse_cookies _exprash_cookies "$HTTP_COOKIE" } # Shutdown trap -trap _exprashShutdown EXIT +trap _exprash_shutdown EXIT # Initialize exprash -_exprashInit +_exprash_init diff --git a/tests/cookies.sh b/tests/cookies.sh index 597e4cb..2aa49e0 100755 --- a/tests/cookies.sh +++ b/tests/cookies.sh @@ -1,20 +1,22 @@ +# shellcheck disable=SC2154,SC2034 + it "Should parse cookies" "$({ HTTP_COOKIE='bob=gob; job=tob' - _exprashInit - output=$(declare -p _exprashCookies) - expected_output='declare -A _exprashCookies=([bob]="gob" [job]="tob" )' + _exprash_init + output=$(declare -p _exprash_cookies) + expected_output='declare -A _exprash_cookies=([bob]="gob" [job]="tob" )' [ "$output" == "$expected_output" ] })" it "Should get cookie value by name" "$({ HTTP_COOKIE='bob=gob; job=tob' - _exprashInit + _exprash_init [ "$(cookie 'bob')" == 'gob' ] })" it "Should determine if a cookie exists by name" "$({ HTTP_COOKIE='bob=gob; job=tob' - _exprashInit - hasCookie 'bob' && ! hasCookie 'kob' + _exprash_init + has_cookie 'bob' && ! has_cookie 'kob' })" diff --git a/tests/query.sh b/tests/query.sh index e3ffac6..24d492f 100644 --- a/tests/query.sh +++ b/tests/query.sh @@ -1,6 +1,8 @@ +# shellcheck disable=SC2154,SC2034 + it "Should parse query string" "$({ QUERY_STRING='ohmy=zsh&exit=vim' - _exprashInit - hasQuery 'ohmy' && ! hasQuery 'ohno' && [ "$(query 'exit')" == 'vim' ] + _exprash_init + has_query 'ohmy' && ! has_query 'ohno' && [ "$(query 'exit')" == 'vim' ] })" diff --git a/tests/routes.sh b/tests/routes.sh index 55d4a3e..dbad6ab 100755 --- a/tests/routes.sh +++ b/tests/routes.sh @@ -1,62 +1,64 @@ -it "Should match plain route" $({ +# shellcheck disable=SC2154,SC2034 + +it "Should match plain route" "$({ PATH_INFO='/plain/route' - _exprashInit + _exprash_init all '/plain/route' -}) +})" -it "Should not match incorrect plain route" $({ +it "Should not match incorrect plain route" "$({ PATH_INFO='/plain/WRONG' - _exprashInit + _exprash_init ! all '/plain/route' -}) +})" -it "Should extract parameter" $({ +it "Should extract parameter" "$({ PATH_INFO='/cats/calico/pet' - _exprashInit - all '/cats/:cat/pet' && hasParam 'cat' && [ "$(param 'cat')" == 'calico' ] -}) + _exprash_init + all '/cats/:cat/pet' && has_param 'cat' && [ "$(param 'cat')" == 'calico' ] +})" -it "Should match wildcard route" $({ +it "Should match wildcard route" "$({ PATH_INFO='/cats/calico/pet' - _exprashInit + _exprash_init all '/cats/*/pet' -}) +})" -it "Should not match incorrect wildcard route" $({ +it "Should not match incorrect wildcard route" "$({ PATH_INFO='/cats/calico/' - _exprashInit + _exprash_init ! all '/cats/*/pet' -}) +})" -it "Should match multi-wildcard route" $({ +it "Should match multi-wildcard route" "$({ PATH_INFO='/cats/calico/pet/donkey' - _exprashInit + _exprash_init all '/cats/**' -}) +})" -it "Should not match incorrect multi-wildcard route" $({ +it "Should not match incorrect multi-wildcard route" "$({ PATH_INFO='/INCORRECT/calico/pet/donkey' - _exprashInit + _exprash_init ! all '/cats/**' -}) +})" -it "Should not match path shorter than route" $({ +it "Should not match path shorter than route" "$({ PATH_INFO='/year' - _exprashInit + _exprash_init ! all '/year/:year' -}) +})" -it "Should match get route" $({ +it "Should match get route" "$({ REQUEST_METHOD='GET' PATH_INFO='/simple/route' - _exprashInit + _exprash_init get '/simple/route' -}) +})" -it "Should not match get route with incorrect method" $({ +it "Should not match get route with incorrect method" "$({ REQUEST_METHOD='POST' PATH_INFO='/simple/route' - _exprashInit + _exprash_init ! get '/simple/route' -}) +})" diff --git a/tests/session.sh b/tests/session.sh index e258354..cb02c1f 100644 --- a/tests/session.sh +++ b/tests/session.sh @@ -1,30 +1,30 @@ # shellcheck disable=SC2154,SC2034 -testSessionDir='/tmp/exprash_test_sessions' -mkdir -p "$testSessionDir" +test_session_dir='/tmp/exprash_test_sessions' +mkdir -p "$test_session_dir" it "Should set session directory" "$({ - _exprashInit - useSession "$testSessionDir" - [ "$_exprashSessionDir" == "$testSessionDir" ] + _exprash_init + use_session "$test_session_dir" + [ "$_exprash_session_dir" == "$test_session_dir" ] })" it "Should save and load session" "$({ - _exprashInit - useSession "$testSessionDir" + _exprash_init + use_session "$test_session_dir" session 'data' 'value' - session_id=$_exprashSessionId - cookie_value=${_exprashSetCookies[$_exprashSessionCookieName]} - _exprashShutdown + session_id=$_exprash_session_id + cookie_value=${_exprash_set_cookies[$_exprash_session_cookie_name]} + _exprash_shutdown # Forcibly clear session data just in case (but it should also get cleared by - # _exprashInit) - _exprashSession=() - _exprashSessionId='' + # _exprash_init) + _exprash_session=() + _exprash_session_id='' - HTTP_COOKIE="${_exprashSessionCookieName}=$cookie_value" - _exprashInit - useSession "$testSessionDir" + HTTP_COOKIE="${_exprash_session_cookie_name}=$cookie_value" + _exprash_init + use_session "$test_session_dir" [ "$(session 'data')" == "value" ] })"