#!/bin/bash # shellcheck disable=SC2034 # false flags nameref params # shellcheck disable=SC2178 # false flags nameref params # ====================== # Required shell options # ====================== # Necessary for piping to "send" funciton shopt -s lastpipe # =============== # Setup Functions # =============== # $1: Log file for redirecting stdout to function redirectStdout() { local output="$1"; [ -z "$output" ] && output="/dev/null"; exec 5<&1; _exprashRedirectStdout="$output"; exec 1>>"$_exprashRedirectStdout"; printf '%s: %s\n' "${REQUEST_METHOD:-UNKNOWN}" "${PATH_INFO:-/}" } # ============= # Route Function # ============= # $1: path function get() { [ "$REQUEST_METHOD" != "GET" ] && return 1; all "$1"; } # $1: path function post() { [ "$REQUEST_METHOD" != "POST" ] && return 1; all "$1"; } # $1: path function put() { [ "$REQUEST_METHOD" != "PUT" ] && return 1; all "$1"; } # $1: path function delete() { [ "$REQUEST_METHOD" != "DELETE" ] && return 1; all "$1"; } # $1: path function all() { [ "$_exprashRouteHandled" -eq 1 ] && return 1; [ -n "$_exprashErrorMessage" ] && return 1; # Reset params _exprashParams=(); _pathMatch "$1" _exprashParams || return 1; _exprashRouteHandled=1; return 0; } function use() { [ "$_exprashRouteHandled" -eq 1 ] && return 1; [ -n "$_exprashErrorMessage" ] && return 1; _exprashRouteHandled=1; return 0; } # ===================== # Error Route Functions # ===================== # $1: path function getError() { [ "$REQUEST_METHOD" != "GET" ] && return 1; allError "$1"; } # $1: path function postError() { [ "$REQUEST_METHOD" != "POST" ] && return 1; allError "$1"; } # $1: path function putError() { [ "$REQUEST_METHOD" != "PUT" ] && return 1; allError "$1"; } # $1: path function deleteError() { [ "$REQUEST_METHOD" != "DELETE" ] && return 1; allError "$1"; } # $1: path function allError() { [ "$_exprashRouteHandled" -eq 1 ] && return 1; [ -z "$_exprashErrorMessage" ] && return 1; # Reset params _exprashParams=(); _pathMatch "$1" _exprashParams || return 1; _exprashRouteHandled=1; return 0; } function useError() { [ "$_exprashRouteHandled" -eq 1 ] && return 1; [ -z "$_exprashErrorMessage" ] && return 1; _exprashRouteHandled=1; return 0; } # ==== # Next # ==== # $1 (optional): error message function next() { _exprashRouteHandled=0; [ -n "$1" ] && _exprashErrorMessage="$1"; } # ============= # App Functions # ============= function errorMessage() { printf '%s' "$_exprashErrorMessage"; } function hasErrorMessage() { [ -n "$_exprashErrorMessage" ]; } # ================= # Request Functions # ================= # $1: param name function param() { printf '%s\n' "${_exprashParams[$1]}"; } function hasParam() { [[ -v "_exprashParams[$1]" ]]; } # $1: key # $2: (optional) index for accessing array parameters function query() { _multiGet _exprashQuery "$1" "$2" } # $1: key function hasQuery() { _multiHas _exprashQuery "$1" } # $1: key 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() { _multiGet _exprashBody "$1" "$2" } # $1: key function hasBody() { _multiHas _exprashBody "$1" } # $1: key function lenBody() { _multiLen _exprashBody "$1" } # $1: key function cookie() { printf '%s' "${_exprashCookies[$1]}" } # $1: key function hasCookie() { [[ -v "_exprashCookies[$1]" ]] } # $1: key # $2: value function setCookie() { _exprashSetCookies["$1"]="$2" } # ================== # Response Functions # ================== function send() { if [ "$_exprashHeadersSent" -eq 0 ]; then sendHeaders _exprashHeadersSent=1 fi _sendRaw } function sendJson() { setHeader 'Content-Type' 'application/json' send } # $1: Path function redirect() { local path="$1" if [ "$path" == 'back' ]; then path="${HTTP_REFERER:-/}" fi status '302' setHeader 'Location' "$path" printf 'Redirecting to: %s' "$path" | send } # $1: satus code function status() { setHeader "Status" "$1" } # $1: Header Name # $2: Header Value function setHeader() { _exprashHeaders["$1"]="$2" } function sendHeaders() { printf '%s\n' "Content-Type: ${_exprashHeaders['Content-Type']}" | _sendRaw for key in "${!_exprashHeaders[@]}"; do [ "$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 # ========= function _sendRaw() { [ -n "$_exprashRedirectStdout" ] && exec >&5; cat; [ -n "$_exprashRedirectStdout" ] && exec 1>>"$_exprashRedirectStdout"; } # $1: path # $2: (nameref) array function _pathToArray() { readarray -t "$2" < <(printf '%s\n' "$1" | tr '/' '\n' | grep .); } # $1: route # $2: (nameref) associative array for params function _pathMatch() { [ -z ${PATH_INFO+x} ] && return 1; [ -z ${1+x} ] && return 1; local path="$PATH_INFO"; local route="$1"; # Params associative array local -n routeParams="$2"; local pathArr; _pathToArray "$path" pathArr; local routeArr; _pathToArray "$route" routeArr; # Get max path length local routeLen=${#routeArr[@]}; local pathLen=${#pathArr[@]}; local maxLen=$(( routeLen >= pathLen ? routeLen : pathLen )); for ((i=0; i "$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 _exprashErrorMessage='' _exprashHeadersSent=0 _exprashHeaders['Content-Type']='text/html' # Parse query string _parseUrlEncoded _exprashQuery "$QUERY_STRING" # Parse cookies _parseCookies _exprashCookies "$HTTP_COOKIE" } # Shutdown trap trap _exprashShutdown EXIT # Initialize exprash _exprashInit