You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
566 lines
11 KiB
566 lines
11 KiB
#!/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 |
|
|
|
|