|
|
|
#!/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<maxLen; i++)); do
|
|
|
|
local routeComponent="${routeArr[$i]}";
|
|
|
|
local pathComponent="${pathArr[$i]}";
|
|
|
|
|
|
|
|
# If route component starts with ":"
|
|
|
|
if [[ "$routeComponent" == :* ]] && [ -n "$pathComponent" ]; then
|
|
|
|
routeParams["${routeComponent:1}"]="$pathComponent";
|
|
|
|
elif [[ "$routeComponent" == '*' ]] && [ -n "$pathComponent" ]; then
|
|
|
|
continue;
|
|
|
|
elif [[ "$routeComponent" == '**' ]] && [ -n "$pathComponent" ]; then
|
|
|
|
break;
|
|
|
|
else
|
|
|
|
# Confirm paths match
|
|
|
|
[ "$routeComponent" != "$pathComponent" ] && return 1;
|
|
|
|
fi;
|
|
|
|
done;
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
# ==================================
|
|
|
|
# Multi-dimensional Parameter Arrays
|
|
|
|
# ==================================
|
|
|
|
|
|
|
|
# $1: (nameref) associative array
|
|
|
|
# $2: key
|
|
|
|
# $3: value
|
|
|
|
function _multiAdd() {
|
|
|
|
local -n multiArr="$1"
|
|
|
|
local key="$2"
|
|
|
|
local value="$3"
|
|
|
|
local i=0
|
|
|
|
while [[ -v "multiArr[$i,$key]" ]]; do
|
|
|
|
(( i++ ))
|
|
|
|
done
|
|
|
|
multiArr["$i,$key"]="$value"
|
|
|
|
}
|
|
|
|
|
|
|
|
# $1: (nameref) associative array
|
|
|
|
# $2: key
|
|
|
|
function _multiLen() {
|
|
|
|
local -n multiArr="$1"
|
|
|
|
local key="$2"
|
|
|
|
local i=0
|
|
|
|
while [[ -v "multiArr[$i,$key]" ]]; do
|
|
|
|
(( i++ ))
|
|
|
|
done
|
|
|
|
printf '%s' "$i"
|
|
|
|
}
|
|
|
|
|
|
|
|
# $1: (nameref) associative array
|
|
|
|
# $2: key
|
|
|
|
function _multiHas() {
|
|
|
|
local -n multiArr="$1"
|
|
|
|
local key="$2"
|
|
|
|
[[ -v "multiArr[0,$key]" ]]
|
|
|
|
}
|
|
|
|
|
|
|
|
# $1: (nameref) associative array
|
|
|
|
# $2: key
|
|
|
|
# $3: index
|
|
|
|
function _multiGet() {
|
|
|
|
local -n multiArr="$1"
|
|
|
|
local key="$2"
|
|
|
|
local i="${3:-0}";
|
|
|
|
printf '%s' "${multiArr[$i,$key]}"
|
|
|
|
}
|
|
|
|
|
|
|
|
# ==================
|
|
|
|
# URL Encoded Parser
|
|
|
|
# ==================
|
|
|
|
|
|
|
|
# $1 | stdin: urlencoded string
|
|
|
|
decodeUri () {
|
|
|
|
local input_str="${1-"$(< /dev/stdin)"}"
|
|
|
|
input_str="${input_str//+/ }";
|
|
|
|
echo -e "${input_str//%/\\x}";
|
|
|
|
}
|
|
|
|
|
|
|
|
# $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%%=*}")
|
|
|
|
value=$(decodeUri "${pair#*=}")
|
|
|
|
if [ -n "$name" ]; then
|
|
|
|
_multiAdd parsedArr "$name" "$value"
|
|
|
|
fi;
|
|
|
|
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"
|
|
|
|
}
|
|
|
|
|
|
|
|
# =======
|
|
|
|
# Session
|
|
|
|
# =======
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
|
|
|
# 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
|
|
|
|
_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
|
|
|
|
|