#!/usr/bin/haserl --shell=/bin/bash --upload-limit=32768 --upload-dir=/www/tmp <%# upload limit: 32Mb %> <% ## SuperGlue project | http://superglue.it | 2014 | GPLv3 ## http://git.superglue.it/superglue/serverfiles ## author: Danja Vasiliev ## ## post.sh - all POST requests are redirected to this script. ## ## examples: ## text: curl --data-urlencode '' http://host/file.html ## image: curl --form "userimage=@file.png" -H "Expect:" http://host/file.png ## command: curl --data-urlencode 'ls' http://host/cmd ## ## returns: 200 (+ output of operation) on success ## 406 (+ error message in debug mode) on error ## ## auth: curl --digest -u admin:changeme ... ## ## no globbing, for safety set -o noglob ## some path variables readonly _WWW='/www' readonly _HTDOCS="${_WWW}/htdocs" readonly _TMP="${_WWW}/tmp" readonly _LOG="${_WWW}/log/post.log" ## multihost, example #if [[ $HTTP_HOST == 'domain.name' ]]; then # _HTDOCS="${_WWW}/domain-name" #fi ## _DEBUG=0 no logging at all ## _DEBUG=1 writes to $_LOG file ## _DEBUG=2 adds a message to HTTP response _DEBUG=1 #### FUNCTIONS ## logging logThis() { [[ "$_DEBUG" -gt 0 ]] || return 0 [[ "$_ERR" -gt 0 ]] && _TYPE='E:' || _TYPE='I:' ## Info or Error indication local _TIME=$(printf '%(%d.%m.%Y %H:%M:%S)T' -1) printf '%b\n' "$_TIME $_TYPE ${1} " >> $_LOG [[ "$_DEBUG" -gt 1 ]] && printf '%b\n' "[verbose] $_TYPE ${1}" return 0 } ## inject function execution trace to global _OUT wTf() { local _WTF="$(printf '%s -> ' '| trace: '${FUNCNAME[*]:1})" _OUT="$_OUT $_WTF" } ## urldecode urlDecode() { local encoded="${1//+/ }" printf '%b' "${encoded//%/\x}" } ## http response headerPrint() { case ${1} in 200) printf '%b' 'HTTP/1.1 200 OK\nAccess-Control-Allow-Origin: *\n\n';; 405) printf '%b' 'HTTP/1.1 405 Method Not Allowed\n\n';; 406) printf '%b' 'HTTP/1.1 406 Not Acceptable\n\n';; esac return 0 } ## takes exit code variable $? and optional "message" string. ## exit code 0 simply falls through. when local message ## is not provided tries to assign global $_OUT. ## ## eg: errorCheck $? "bad zombie" ## ## produces HTTP 406 header, $_OUT message, triggers logThis() ## and exits the main loop with exit >= 1. errorCheck() { _ERR=${1} ## exit code [[ $_ERR -gt 0 ]] || return 0 local _MSG=${2} ## if $_OUT is present cut it down to one line ## otherwise assign message from the invokation arguments [[ $_OUT ]] && _OUT="${_OUT%%$'\n'*}" || { _OUT=${_MSG:='unknown error occured'}; wTf; } [[ -e $_POST_TMP ]] && rm -f $_POST_TMP headerPrint '406' logThis "${_OUT}"; exit $_ERR } ## urlencoded POST dispatcher postUrlenc() { ## set vars found in POST setQueryVars case "${_REQUEST_URI}" in \/cmd) postCmd ;; ## handle /cmd POST *) postHtml ;; ## handle html POST esac } ## handle /cmd POST postCmd() { local _CMD=( ${_POST} ) ## convert POST to array [[ ${#_CMD[@]} -lt 5 ]] || errorCheck '1' "'${_CMD[*]}': too many arguments" local _EXE="${_CMD[0]}" ## first member is command local _ARG="${_CMD[@]:1}" ## the rest is arguments ## note unquoted regex [[ ! "$_ARG" =~ (\.\.|^/| /) ]] || errorCheck '1' "'$_ARG': illegal path" ## 'ls' replacement function lss() { _D='\t' ## do we want a customizable delimiter? while getopts 'la' _OPT; do case $_OPT in l) local _LNG="$_D%F$_D%s$_D%y$_D%U$_D%G$_D%a" ;; a) shopt -s dotglob esac done shift $((OPTIND-1)) ## removing used args [[ -z "${@}" ]] && _PT="./*" ## list ./* if called with no args [[ -d "${@}" ]] && _PT="/*" ## add /* to directories ## if error occures return 0 stat --printf "%n$_LNG\n" -- "${@%%/}"$_PT 2>/dev/null || _ERR=0 return $_ERR } case "$_EXE" in ls|lss) _EXE="lss"; _ARG="${_ARG}" ;; ## no error is returned cp) _ARG="${_ARG}" ;; rm) _ARG="${_ARG}" ;; ## add recursive option if you need mv) _ARG="${_ARG}" ;; mkdir) _ARG="${_ARG}" ;; log) _EXE="tail"; _ARG="${_ARG} ${_LOG}" ;; wget) _ARG="-q ${_ARG/ */} -O ${_ARG/* /}" ;; ## quiet *) errorCheck '1' "'$_EXE': bad command" ;; esac ## toggle globbing set +o noglob _OUT=$($_EXE $_ARG 2>&1) _ERR=$? ## toggle globbing set -o noglob logThis "$_EXE $_ARG" errorCheck $_ERR } ## handle html POST postHtml() { ## save POST to file _OUT=$( (printf '%b' "${_POST}" > "${_HTDOCS}${_REQUEST_URI}") 2>&1) _ERR=$? errorCheck $_ERR } setQueryVars() { _VARS=( ${!POST_*} ) local v for v in ${_VARS[@]}; do logThis "$v=${!v}" done } ## octet POST dispatcher postOctet() { ## get 'data:' header length local IFS=','; read -d',' -r _DH < $_POST_TMP case "${_ENC}" in base64) postBase64Enc;; binary) postBinary ;; *) postGuessEnc ;; ## handle data POST esac } ## to be converted into a proper data-type detection function postGuessEnc() { shopt -s nocasematch local _DTP="^.*\;([[:alnum:]].+)$" ## data-type header pattern ## look for encoding in the data header [[ "${_DH}" =~ ${_DTP} ]] && _ENC="${BASH_REMATCH[1]}" logThis "'$_ENC:' encoding is the best guess"; shopt -u nocasematch case "$_ENC" in base64) postBase64Enc ;; ## binary) _ERR=1 ;; ## json) _ERR=1 ;; ## quoted-printable) _ERR=1 ;; *) _ERR=1; _OUT="'${_ENC:='unknown'}' encoding, unknown POST failed";; esac errorCheck $_ERR } ## handle base64 post postBase64Enc() { logThis "'${_ENC}:' decoding stream" _DL=${#_DH} ## get data-header length [[ $_DL -lt 10 ]] && { _DL=23; _SKP=0; } || { let _DL+=1; _SKP=1; } ## '23' - what?! ## the line below seems to be the best solution for the time being ## dd 'ibs' and 'iflags' seem not to work on OpenWRT - investigate as it might be very useful _OUT=$( dd if=${_POST_TMP} bs=${_DL} skip=${_SKP} | base64 -d > "${_HTDOCS}${_REQUEST_URI}" 2>&1) _ERR=$? errorCheck $_ERR } postBinary() { logThis "'binary': decoding stream" ## it is unclear what will be necessary to do here _OUT=$( dd if="${_POST_TMP}" of="${_HTDOCS}${_REQUEST_URI}" 2>&1 ) _ERR=$? errorCheck $_ERR } postMpart() { logThis "'multipart': decoding stream" local _BND=$(findPostOpt 'boundary') ## bash is binary unsafe and eats away precious lines ## thus using gawk function cutFile() { gawk -v "want=$1" -v "bnd=$_BND" ' BEGIN { RS="\r\n"; ORS="\r\n" } # reset based on boundaries $0 == "--"bnd"" { st=1; next; } $0 == "--"bnd"--" { st=0; next; } $0 == "--"bnd"--\r" { st=0; next; } # search for wanted file st == 1 && $0 ~ "^Content-Disposition:.* name=\""want"\"" { st=2; next; } st == 1 && $0 == "" { st=9; next; } # wait for newline, then start printing st == 2 && $0 == "" { st=3; next; } st == 3 { print $0 } ' 2>&1 } cutFile 'userimage' < "${_POST_TMP}" > "${_HTDOCS}${_REQUEST_URI}" _ERR=$? errorCheck $_ERR } ## find arbitrary option supplied in Content-Type header ## eg: "Content-Type:application/octet-stream; verbose=1" findPostOpt() { for i in "${CONTENT_TYPE[@]:1}"; do case "${i/=*}" in "$1") printf '%b' "${i/*=}" ;; esac done return 0 } ## sanitize by backslashing all expandable symbols escapeStr() { printf "%q" "${*}" } ## brutally replace unwanted characters cleanFname() { shopt -s extglob local _STR="${*}" echo -n "${_STR//[^[:alnum:]._\-\/\\]/_}" shopt -u extglob } #### MAIN LOOP logThis 'yes' ## timing ## TODO: remove it ## run once here and once at the end #read t z < /proc/uptime ## check if we are in $_HTDOCS directory cd $_HTDOCS || errorCheck $? 'htdocs unavailable' [[ "${PWD}" == "${_HTDOCS}" ]] || errorCheck $? 'htdocs misconfigured' if [[ "${REQUEST_METHOD^^}" == "POST" ]]; then [[ $CONTENT_LENGTH -gt 0 ]] || err 'content length is zero, 301 back to referer' '301' case "${CONTENT_TYPE^^}" in APPLICATION/X-WWW-FORM-URLENCODED*) setQueryVars;; MULTIPART/FORM-DATA*) getQueryFile;; *) _ERR=1; _OUT='this is not a post';; esac case $REQUEST_URI in *cmd) pwdChange;; *) logThis 'bad action'; headerPrint 405; echo 'no such thing'; exit 1;; esac fi ## URI is considered as a file dest to work with ## add 'index.html' to default and empty request uri _REQUEST_URI="${REQUEST_URI/%\///index.html}" _REQUEST_URI="$(urlDecode $_REQUEST_URI)" _PATH="${_REQUEST_URI%/*}" CONTENT_TYPE=( ${CONTENT_TYPE} ) _CONTENT_TYPE="${CONTENT_TYPE[0]/;}" _ENC="${HTTP_CONTENT_ENCODING}" #logThis "Len: $CONTENT_LENGTH Ctype: $CONTENT_TYPE Enc: $_CONTENT_ENCODING" ## check for 'verbose' option in POST #findPostOpt 'verbose' || { _DEBUG=2; logThis 'verbose mode is requested'; } logThis 'yes 2' #_POST_TMP=$(mktemp -p $_TMP) ## make tmp POST file #cat > $_POST_TMP ## cautiously storing entire POST in a file ## dispatching POST case "${_CONTENT_TYPE}" in application\/x-www-form-urlencoded) postUrlenc ;; application\/octet-stream) postOctet ;; multipart/form-data) postMpart ;; *) _ERR=1; _OUT='this is not a post' ;; esac #[[ -e $_POST_TMP ]] && rm -f $_POST_TMP ## make sure we are good errorCheck $_ERR [[ -z $_OUT ]] || _OUT="${_OUT}\n" headerPrint '200' ## on success printf '%b' "${_OUT}" logThis 'OK 200' #read d z < /proc/uptime #logThis $((${d/./}-${t/./}))"/100s" exit 0 %>